Documentation
¶
Index ¶
- Constants
- Variables
- func AllEventTypes() []string
- func BindCommand[E InstanceEvent, C cqrs.Action](bus event.Bus, commands cqrs.Bus, ...) (event.Unsubscribe, error)
- func IsSuperAdmin(p *security.Principal) bool
- func PayloadOccurredAt(e DomainEvent) timex.DateTime
- func SubscribeInstance[T InstanceEvent](bus event.Bus, ...) (event.Unsubscribe, error)
- func ValidateBusinessIdentifier(id string) error
- type ActionLog
- type ActionType
- type Activity
- type AddAssigneeType
- type AggregateKind
- type Aggregator
- type ApprovalMethod
- type ApprovalNodeData
- type AssigneeDefinition
- type AssigneeKind
- type AssigneeService
- type AssigneesAddedEvent
- type AssigneesRemovedEvent
- type BaseNodeData
- type BindingMode
- type BindingProjectionStatus
- type BindingTrigger
- type BusinessBindingConfig
- type BusinessProjection
- type BusinessRecordKey
- type BusinessRefProvider
- type BusinessRefResolver
- type CCDefinition
- type CCKind
- type CCNodeData
- type CCNotifiedEvent
- type CCRecipient
- type CCRecord
- type CCTiming
- type CallerContext
- type ColumnDataType
- type Condition
- type ConditionBranch
- type ConditionEvaluator
- type ConditionGroup
- type ConditionKind
- type ConditionNodeData
- type ConditionOperator
- type ConsecutiveApproverAction
- type Delegation
- type DomainEvent
- type EdgeDefinition
- type EmptyAssigneeAction
- type EndNodeData
- type EvaluationContext
- type ExecutionType
- type FieldKind
- type FieldOption
- type Flow
- type FlowCategory
- type FlowCreatedEvent
- type FlowDefinition
- type FlowDeployedEvent
- type FlowEdge
- type FlowEventBase
- type FlowGraphEdge
- type FlowGraphNode
- type FlowGraphNodeData
- type FlowInitiator
- type FlowNode
- type FlowNodeAssignee
- type FlowNodeCC
- type FlowPublishedEvent
- type FlowToggledEvent
- type FlowUpdatedEvent
- type FlowVersion
- type FormData
- type FormFieldDefinition
- type FormSchemaParser
- type FormSnapshot
- type FormTable
- type FormTableColumn
- type HandleNodeData
- type InitiatorKind
- type Instance
- type InstanceBindingFailedEvent
- type InstanceCompletedEvent
- type InstanceCreatedEvent
- type InstanceEvent
- type InstanceEventBase
- type InstanceFilter
- type InstanceFlowGraph
- type InstanceGlobalsResolver
- type InstanceLifecycleHook
- type InstanceNoGenerator
- type InstanceResubmittedEvent
- type InstanceReturnedEvent
- type InstanceRolledBackEvent
- type InstanceStatus
- type InstanceSubscribeOption
- type InstanceWithdrawnEvent
- type NodeAutoPassedEvent
- type NodeData
- type NodeDefinition
- type NodeKind
- type NodeParticipant
- type NodeProgressStatus
- type NodeVisit
- type NodeVisitStatus
- type PassRule
- type PassRuleContext
- type PassRuleResult
- type PassRuleStrategy
- type Permission
- type Position
- type PrincipalDepartmentResolver
- type PrincipalTenantResolver
- type ResolvedAssignee
- type RoleMembershipChecker
- type RollbackDataStrategy
- type RollbackType
- type SameApplicantAction
- type StartNodeData
- type StorageMode
- type Task
- type TaskApprovedEvent
- type TaskCanceledEvent
- type TaskCreatedEvent
- type TaskDeadlineWarningEvent
- type TaskEventBase
- type TaskHandledEvent
- type TaskNodeData
- type TaskReassignedEvent
- type TaskRejectedEvent
- type TaskStatus
- type TaskTimedOutEvent
- type TaskTransferredEvent
- type TaskUrgedEvent
- type TimelineEntry
- type TimelineEntryKind
- type TimeoutAction
- type UrgeRecord
- type UserInfo
- type UserInfoResolver
- type ValidationRule
- type VersionStatus
Constants ¶
const ( EventTypeInstanceCreated = "approval.instance.created" EventTypeInstanceCompleted = "approval.instance.completed" EventTypeInstanceWithdrawn = "approval.instance.withdrawn" EventTypeInstanceRolledBack = "approval.instance.rolled_back" EventTypeInstanceReturned = "approval.instance.returned" EventTypeInstanceResubmitted = "approval.instance.resubmitted" EventTypeInstanceBindingFailed = "approval.instance.binding_failed" EventTypeNodeAutoPassed = "approval.node.auto_passed" EventTypeTaskCreated = "approval.task.created" EventTypeTaskApproved = "approval.task.approved" EventTypeTaskHandled = "approval.task.handled" EventTypeTaskRejected = "approval.task.rejected" EventTypeTaskCanceled = "approval.task.canceled" EventTypeTaskTransferred = "approval.task.transferred" EventTypeTaskReassigned = "approval.task.reassigned" EventTypeTaskTimedOut = "approval.task.timed_out" EventTypeAssigneesAdded = "approval.task.assignees_added" EventTypeAssigneesRemoved = "approval.task.assignees_removed" EventTypeTaskDeadlineWarning = "approval.task.deadline_warning" EventTypeTaskUrged = "approval.task.urged" EventTypeCCNotified = "approval.cc.notified" EventTypeFlowCreated = "approval.flow.created" EventTypeFlowUpdated = "approval.flow.updated" EventTypeFlowDeployed = "approval.flow.deployed" EventTypeFlowToggled = "approval.flow.toggled" EventTypeFlowPublished = "approval.flow.published" )
Approval event type identifiers. Exposed as constants so framework callers (route inspection, subscription filters, metrics labels) can reference them by symbol rather than risking a typo on the wire string.
const ( DefaultExecutionType = ExecutionManual DefaultApprovalMethod = ApprovalParallel DefaultPassRule = PassAll DefaultEmptyAssigneeAction = EmptyAssigneeAutoPass DefaultSameApplicantAction = SameApplicantSelfApprove DefaultConsecutiveApproverAction = ConsecutiveApproverNone DefaultRollbackType = RollbackPrevious DefaultRollbackDataStrategy = RollbackDataKeep DefaultTimeoutAction = TimeoutActionNone DefaultCCTiming = CCTimingAlways // Handle nodes default to sequential execution with the PassAny rule, // since any handler completing the task is sufficient. DefaultHandleApprovalMethod = ApprovalSequential DefaultHandlePassRule = PassAny // DefaultUrgeCooldownMinutes is the urge cooldown applied when a node // leaves UrgeCooldownMinutes at 0. The flow editor surfaces the same // value in its placeholder text; keep the two in lockstep. DefaultUrgeCooldownMinutes = 30 )
Designer-aligned defaults resolved by ApplyTo when a field is omitted from the node data payload. The flow editor displays exactly these values for untouched controls, so resolving them here keeps "what the designer shows" and "what the engine runs" identical by construction.
const ActivityUrge = "urge"
ActivityUrge is the Activity.Action value for urge records — the one vocabulary member with no ActionType counterpart, because urges are persisted as urge records rather than action logs. Every other Action value is an ActionType string verbatim.
const DefaultTenantID = "default"
DefaultTenantID is the tenant identifier used when a caller does not carry an explicit tenant — the conventional single-tenant deployment value.
const SuperAdminRole = "approval:super_admin"
SuperAdminRole is the role string that grants cross-tenant access to admin queries and operations. Hosts assign this role to platform-level operators that legitimately need to act across tenants (audit teams, billing, etc.). Without it, admin endpoints reject requests that lack a tenant filter — guarding against accidental cross-tenant data exposure.
Variables ¶
var ( // ErrUnknownNodeKind reports a node definition whose kind is not one of // the NodeKind enum values. ErrUnknownNodeKind = errors.New("unknown node kind") // ErrNodeDataUnmarshal reports node data JSON that failed to decode into // the kind's typed payload. ErrNodeDataUnmarshal = errors.New("node data unmarshal failed") )
var ErrAnonymousSubscriberGroup = errors.New(
"approval: anonymous handler cannot derive a stable consumer group; pass approval.WithGroup")
ErrAnonymousSubscriberGroup is returned by SubscribeInstance when the handler is an anonymous function and no explicit group was supplied. The consumer group is the subscriber's durable identity (Redis XGROUP name, Inbox dedupe scope); an anonymous function's runtime name carries a positional counter (func1, func2, …) that silently changes when code moves, so it can never be a stable identity.
var ErrCrossTenantAccess = errors.New("approval: cross-tenant access denied")
ErrCrossTenantAccess is returned when a non-super-admin caller attempts to act on an entity owned by a different tenant. Resource and command handlers use CallerContext.Authorize to surface it consistently.
var ErrDerivedGroupConflict = errors.New(
"approval: derived consumer group already registered in this process; pass approval.WithGroup to disambiguate")
ErrDerivedGroupConflict is returned when two subscriptions in the same process derive the same consumer group — almost always the same method subscribed twice. Explicit groups are exempt: sharing a named group is a legitimate load-balancing choice.
var ErrInvalidBusinessIdentifier = errors.New("approval: invalid business identifier (must match ^[A-Za-z_][A-Za-z0-9_]{0,62}$)")
ErrInvalidBusinessIdentifier is returned by ValidateBusinessIdentifier for values that do not match a SQL-safe identifier pattern. Flow CRUD validation surfaces it to operators; the write-back re-checks the same rule as defense-in-depth.
var ErrNonCommandAction = errors.New(
"approval: BindCommand requires a command action type; queries cannot be bound to instance events")
ErrNonCommandAction is returned by BindCommand when the bound action type is a query. The bridge exists to turn approval facts into side effects; a query has no side effect to trigger, so binding one is always a programming error.
var ErrUnnamedCommandType = errors.New(
"approval: bound command must be a named concrete type")
ErrUnnamedCommandType is returned when the bound command type is not a named concrete type (an anonymous struct or an interface). Commands are the stable vocabulary of a host: the bridge derives the consumer group, log lines, and the dispatch key from the type's identity, which such types do not have.
var SystemCaller = CallerContext{IsSystemInternal: true}
SystemCaller is the canonical CallerContext for callers that bypass tenant scoping by carrying IsSystemInternal. In the current tree its only consumers are test fixtures (production system paths operate on trusted, pre-scoped rows without a CallerContext); use it instead of the zero value so the bypass intent is explicit at the call site.
Functions ¶
func AllEventTypes ¶ added in v0.29.0
func AllEventTypes() []string
AllEventTypes returns every approval domain event type identifier. It is the canonical, enumerable registry of the event surface: callers that must stay exhaustive over the event set (e.g. the start-up routing check, which asserts each transactional event resolves to a transactional transport) derive from this list so adding a new event constant cannot silently slip past them. Keep this in sync with the EventType* constants above — the only reason a new constant would be omitted is a deliberate, reviewed decision.
func BindCommand ¶ added in v0.38.0
func BindCommand[E InstanceEvent, C cqrs.Action]( bus event.Bus, commands cqrs.Bus, mapper func(evt E, env event.Envelope) (cmd C, ok bool), opts ...InstanceSubscribeOption, ) (event.Unsubscribe, error)
BindCommand subscribes to instance event E and dispatches the mapped command C through the host's CQRS bus — the declarative bridge from approval facts to host side effects. mapper is a pure translation: it shapes the command from the event plus its delivery Envelope and reports whether the event is relevant (ok=false acknowledges without dispatching). Business logic belongs in the command handler, which runs the host's full behavior pipeline (transaction, audit, validation); the handler's result is discarded — dispatch is fire-and-record, results belong to request/response callers.
The consumer group defaults to the command type's identity ("vef:cmd:" + module-relative package + type name) — renaming or moving the command deliberately re-keys the subscription, exactly like renaming a SubscribeInstance handler. Binding the same command type twice in one process fails with ErrDerivedGroupConflict; WithGroup disambiguates (or pins the group ahead of a rename). Filters (ForFlows / ForTenants) and WithConcurrency apply as in SubscribeInstance.
Delivery inherits the event route's semantics: an at-least-once transport (outbox / redis_stream) can redeliver, so the command handler must be idempotent — copy Envelope.ID (the Inbox dedupe key, stable across redeliveries) into the command when the handler needs a dedupe key of its own. Unlike the eventual business projection — which converges on the latest desired state and may skip intermediate statuses — every instance transition dispatches its own command, making this the lane for side effects tied to a specific lifecycle moment.
func IsSuperAdmin ¶ added in v0.24.0
IsSuperAdmin reports whether the principal carries the cross-tenant override role. Nil principal returns false.
func PayloadOccurredAt ¶ added in v0.24.0
func PayloadOccurredAt(e DomainEvent) timex.DateTime
PayloadOccurredAt extracts the OccurredTime field from a DomainEvent payload. Every in-tree approval event struct carries this field; publishers use the value to project business time onto Envelope.OccurredAt via event.WithOccurredAt. Returns the zero DateTime for payloads that lack the field (defensive — should never happen for in-tree events).
Exposed as a package-level helper rather than a method on DomainEvent so the interface can stay minimal (EventType only) while still letting transports / behaviors project business time.
func SubscribeInstance ¶ added in v0.37.0
func SubscribeInstance[T InstanceEvent]( bus event.Bus, handler func(ctx context.Context, evt T, env event.Envelope) error, opts ...InstanceSubscribeOption, ) (event.Unsubscribe, error)
SubscribeInstance subscribes a typed handler to one instance event type with declarative routing filters. Events whose envelope does not match every filter are acknowledged without invoking the handler — the filters answer "is this instance mine?", while business predicates (final status, form values) stay in the handler body. The handler also receives the delivery Envelope: Envelope.ID is the Inbox dedupe key, stable across redeliveries, and therefore the key to build manual idempotency on when the route is at-least-once.
The consumer group defaults to a name derived from the handler's method identity, normalized into the same "vef:<scope>:<name>" shape as the framework's other groups: "smp/internal/mms.(*RightApplicationSvc).Handle-fm" becomes "vef:sub:internal/mms.RightApplicationSvc.Handle" (the main module prefix is stripped). Package-level named functions derive the same way; anonymous functions have no stable name and fail with ErrAnonymousSubscriberGroup unless WithGroup is given. Renaming or moving a handler changes its derived group — a new XGROUP starts and the old one is orphaned — so pin the group with WithGroup before such refactors. Deriving the same group twice in one process fails with ErrDerivedGroupConflict.
func ValidateBusinessIdentifier ¶ added in v0.24.0
ValidateBusinessIdentifier reports whether id is a safe SQL identifier for use as a dynamic table or column name in business binding queries. Empty / whitespace-only strings pass — the caller decides whether absence is itself an error (see Flow validation paths for the policy).
Types ¶
type ActionLog ¶
type ActionLog struct {
orm.BaseModel `bun:"table:apv_action_log,alias:aal"`
orm.Model
orm.CreationTrackedModel
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID *string `json:"nodeId" bun:"node_id,nullzero"`
TaskID *string `json:"taskId" bun:"task_id,nullzero"`
Action ActionType `json:"action" bun:"action"`
OperatorID string `json:"operatorId" bun:"operator_id"`
OperatorName string `json:"operatorName" bun:"operator_name"`
OperatorDepartmentID *string `json:"operatorDepartmentId" bun:"operator_department_id,nullzero"`
OperatorDepartmentName *string `json:"operatorDepartmentName" bun:"operator_department_name,nullzero"`
IPAddress *string `json:"ipAddress" bun:"ip_address,nullzero"`
UserAgent *string `json:"userAgent" bun:"user_agent,nullzero"`
Opinion *string `json:"opinion" bun:"opinion,nullzero"`
TransferToID *string `json:"transferToId" bun:"transfer_to_id,nullzero"`
TransferToName *string `json:"transferToName" bun:"transfer_to_name,nullzero"`
TransferToDepartmentID *string `json:"transferToDepartmentId" bun:"transfer_to_department_id,nullzero"`
TransferToDepartmentName *string `json:"transferToDepartmentName" bun:"transfer_to_department_name,nullzero"`
RollbackToNodeID *string `json:"rollbackToNodeId" bun:"rollback_to_node_id,nullzero"`
AddAssigneeType *AddAssigneeType `json:"addAssigneeType" bun:"add_assignee_type,nullzero"`
AddedAssignees []UserInfo `json:"addedAssignees" bun:"added_assignees,type:jsonb"`
RemovedAssignees []UserInfo `json:"removedAssignees" bun:"removed_assignees,type:jsonb"`
CCUsers []UserInfo `json:"ccUsers" bun:"cc_users,type:jsonb"`
Attachments []string `json:"attachments" bun:"attachments,type:jsonb,nullzero"`
Meta map[string]any `json:"meta" bun:"meta,type:jsonb,nullzero"`
}
ActionLog represents an action log entry. Person lists (added / removed assignees, CC recipients) are stored as UserInfo arrays so each person carries id, name, and the department snapshotted at action time — no parallel-array zipping.
func (*ActionLog) TransferTo ¶ added in v0.35.0
TransferTo returns the transfer recipient as a person snapshot, or nil when the action carried no transfer target.
type ActionType ¶
type ActionType string
ActionType represents the type of action performed by an operator.
const ( ActionSubmit ActionType = "submit" ActionApprove ActionType = "approve" ActionHandle ActionType = "handle" ActionReject ActionType = "reject" ActionTransfer ActionType = "transfer" ActionWithdraw ActionType = "withdraw" ActionCancel ActionType = "cancel" ActionRollback ActionType = "rollback" ActionAddAssignee ActionType = "add_assignee" ActionRemoveAssignee ActionType = "remove_assignee" ActionExecute ActionType = "execute" // System execution action ActionResubmit ActionType = "resubmit" // Resubmit a returned instance ActionReassign ActionType = "reassign" // Admin reassigned task to a different user ActionTerminate ActionType = "terminate" // Admin force-terminated an instance ActionAddCC ActionType = "add_cc" // Participant added CC recipients )
type Activity ¶ added in v0.35.0
type Activity struct {
Action string `json:"action"`
Operator UserInfo `json:"operator"`
Opinion *string `json:"opinion,omitempty"`
Attachments []string `json:"attachments,omitempty"`
TransferTo *UserInfo `json:"transferTo,omitempty"`
Target *UserInfo `json:"target,omitempty"`
RollbackToNodeID *string `json:"rollbackToNodeId,omitempty"`
RollbackToNodeName *string `json:"rollbackToNodeName,omitempty"`
AddedAssignees []UserInfo `json:"addedAssignees,omitempty"`
RemovedAssignees []UserInfo `json:"removedAssignees,omitempty"`
CCUsers []UserInfo `json:"ccUsers,omitempty"`
CreatedAt timex.DateTime `json:"createdAt"`
}
Activity is a side action recorded at a node: who did what, when, and the action-specific details. Action carries the ActionType string (transfer / rollback / add_assignee / remove_assignee / add_cc / reassign / execute / submit / resubmit / withdraw / terminate) plus ActivityUrge for urge records. Opinion holds the action's free text (a transfer reason, a withdraw reason, an urge message); Target names the counterpart of a directed action (the urged assignee). Decisions themselves (approve / handle / reject) are not repeated here — they live on the participant that made them. Shared by the timeline and the flow-graph projections.
type AddAssigneeType ¶
type AddAssigneeType string
AddAssigneeType represents the type of dynamic assignee addition. It defines how a newly added assignee is positioned relative to the current task.
const ( AddAssigneeBefore AddAssigneeType = "before" // Before: new assignee processes first, original task becomes pending after completion AddAssigneeAfter AddAssigneeType = "after" // After: new assignee processes after the original assignee completes AddAssigneeParallel AddAssigneeType = "parallel" // Parallel: new assignee joins the current parallel group to process together )
func (AddAssigneeType) IsValid ¶
func (t AddAssigneeType) IsValid() bool
IsValid checks if the AddAssigneeType is a valid value.
func (*AddAssigneeType) UnmarshalJSON ¶
func (t *AddAssigneeType) UnmarshalJSON(data []byte) error
UnmarshalJSON validates AddAssigneeType values when decoding JSON payloads.
type AggregateKind ¶ added in v0.36.0
type AggregateKind string
AggregateKind enumerates the aggregations a field condition may apply over a detail-table field's rows. Semantics follow SQL aggregates: count is the row count, sum of an empty table is 0, and avg over an empty table matches no comparison (NULL semantics) instead of being 0. Folds compute in float64 — prefer ordering operators over eq/ne when comparing sums or averages of fractional amounts.
const ( AggregateSum AggregateKind = "sum" AggregateCount AggregateKind = "count" AggregateAvg AggregateKind = "avg" )
func (AggregateKind) FoldsColumn ¶ added in v0.36.0
func (a AggregateKind) FoldsColumn() bool
FoldsColumn reports whether the aggregate reduces a numeric column (sum / avg) or the rows themselves (count). It is the single source of the column contract: deploy validation derives the "column required / forbidden" rule from it and the evaluator derives what to extract, so a new aggregate kind declares its shape here once and both sides follow.
func (AggregateKind) IsValid ¶ added in v0.36.0
func (a AggregateKind) IsValid() bool
IsValid reports whether the aggregate is one of the defined values.
type Aggregator ¶ added in v0.36.0
type Aggregator interface {
// Kind returns the aggregate kind this implementation folds.
Kind() AggregateKind
// Fold reduces the extracted column values (for column aggregates) or
// the row count (for row aggregates such as count) into the comparison
// operand. matchable=false means the aggregate has no defined value for
// the input — e.g. avg over zero rows — and the condition must not
// match, mirroring SQL NULL comparison semantics.
Fold(values []float64, rowCount int) (result float64, matchable bool)
}
Aggregator folds a detail-table field's rows into one comparable number for an aggregate field condition. Implementations are collected through the FX group "vef:approval:aggregators" — adding an aggregate (built-in or host-supplied) registers a new implementation; the evaluator, the deploy validation, and existing aggregators stay untouched.
type ApprovalMethod ¶
type ApprovalMethod string
ApprovalMethod represents the method of approval for a node with multiple assignees. It defines how the approval decision is made when there are multiple approvers.
const ( ApprovalSequential ApprovalMethod = "sequential" // Sequential: approvers process one by one in order, all must approve ApprovalParallel ApprovalMethod = "parallel" // Parallel: approvers process simultaneously, decision based on consensus rules )
func (ApprovalMethod) IsValid ¶ added in v0.29.0
func (m ApprovalMethod) IsValid() bool
IsValid reports whether the approval method is one of the defined values.
type ApprovalNodeData ¶
type ApprovalNodeData struct {
BaseNodeData
TaskNodeData
ApprovalMethod ApprovalMethod `json:"approvalMethod,omitempty"`
PassRule PassRule `json:"passRule,omitempty"`
PassRatio decimal.Decimal `json:"passRatio"`
SameApplicantAction SameApplicantAction `json:"sameApplicantAction,omitempty"`
ConsecutiveApproverAction ConsecutiveApproverAction `json:"consecutiveApproverAction,omitempty"`
RollbackType RollbackType `json:"rollbackType,omitempty"`
RollbackDataStrategy RollbackDataStrategy `json:"rollbackDataStrategy,omitempty"`
RollbackTargetKeys []string `json:"rollbackTargetKeys,omitempty"`
IsRollbackAllowed *bool `json:"isRollbackAllowed,omitempty"`
IsAddAssigneeAllowed *bool `json:"isAddAssigneeAllowed,omitempty"`
AddAssigneeTypes []AddAssigneeType `json:"addAssigneeTypes,omitempty"`
IsRemoveAssigneeAllowed *bool `json:"isRemoveAssigneeAllowed,omitempty"`
IsManualCCAllowed *bool `json:"isManualCcAllowed,omitempty"`
}
ApprovalNodeData contains data specific to approval nodes.
The Is*Allowed permission toggles are pointers because their default is true; see TaskNodeData for the rationale.
func (*ApprovalNodeData) ApplyTo ¶
func (d *ApprovalNodeData) ApplyTo(node *FlowNode)
ApplyTo applies approval node data to a FlowNode, resolving omitted fields to the designer defaults. The target node is assumed to be freshly constructed (zero value); all fields are overwritten unconditionally as a full-snapshot deploy operation.
func (*ApprovalNodeData) Kind ¶
func (*ApprovalNodeData) Kind() NodeKind
Kind returns the node kind.
type AssigneeDefinition ¶
type AssigneeDefinition struct {
Kind AssigneeKind `json:"kind"`
IDs []string `json:"ids,omitempty"`
FormField *string `json:"formField,omitempty"`
SortOrder int `json:"sortOrder"`
}
AssigneeDefinition represents an assignee configuration in the flow definition.
type AssigneeKind ¶
type AssigneeKind string
AssigneeKind represents the kind of assignee.
const ( AssigneeUser AssigneeKind = "user" AssigneeRole AssigneeKind = "role" AssigneeDepartment AssigneeKind = "department" // Department head AssigneeSelf AssigneeKind = "self" // Applicant themselves AssigneeSuperior AssigneeKind = "superior" // Direct superior AssigneeDepartmentLeader AssigneeKind = "department_leader" // Leaders of the applicant's own department (single level) AssigneeFormField AssigneeKind = "form_field" // Based on form field )
func (AssigneeKind) IsValid ¶ added in v0.29.0
func (k AssigneeKind) IsValid() bool
IsValid reports whether the assignee kind is one of the defined values.
type AssigneeService ¶
type AssigneeService interface {
// GetSuperior returns the direct superior's user info for the given user.
GetSuperior(ctx context.Context, userID string) (*UserInfo, error)
// GetDepartmentLeaders returns the leader user info for the given department.
GetDepartmentLeaders(ctx context.Context, departmentID string) ([]UserInfo, error)
// GetRoleUsers returns all user info for users that have the given role.
GetRoleUsers(ctx context.Context, roleID string) ([]UserInfo, error)
}
AssigneeService resolves approval assignees from organizational data (implemented by host app).
type AssigneesAddedEvent ¶
type AssigneesAddedEvent struct {
TaskEventBase
AddType AddAssigneeType `json:"addType"`
Assignees []UserInfo `json:"assignees"`
}
AssigneesAddedEvent fired when assignees are dynamically added. TaskID is the task whose assignee initiated the addition.
func NewAssigneesAddedEvent ¶
func NewAssigneesAddedEvent(instance *Instance, task *Task, node *FlowNode, addType AddAssigneeType, assignees []UserInfo) *AssigneesAddedEvent
func (*AssigneesAddedEvent) EventType ¶ added in v0.24.0
func (*AssigneesAddedEvent) EventType() string
type AssigneesRemovedEvent ¶
type AssigneesRemovedEvent struct {
TaskEventBase
Assignees []UserInfo `json:"assignees"`
}
AssigneesRemovedEvent fired when assignees are dynamically removed. TaskID is the removed assignee's task.
func NewAssigneesRemovedEvent ¶
func NewAssigneesRemovedEvent(instance *Instance, task *Task, node *FlowNode, assignees []UserInfo) *AssigneesRemovedEvent
func (*AssigneesRemovedEvent) EventType ¶ added in v0.24.0
func (*AssigneesRemovedEvent) EventType() string
type BaseNodeData ¶
type BaseNodeData struct {
Name string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
BaseNodeData contains common fields shared across all node data types.
func (BaseNodeData) GetDescription ¶
func (d BaseNodeData) GetDescription() *string
GetDescription returns the node description.
type BindingMode ¶
type BindingMode string
BindingMode represents the mode of binding with business data. It defines how the approval workflow stores and associates form data.
const ( BindingStandalone BindingMode = "standalone" // Standalone: form data is stored in the approval workflow's own table BindingBusiness BindingMode = "business" // Business: links to existing business data table )
func (BindingMode) IsValid ¶ added in v0.36.0
func (m BindingMode) IsValid() bool
IsValid checks if the BindingMode is a valid value.
type BindingProjectionStatus ¶ added in v0.38.0
type BindingProjectionStatus string
BindingProjectionStatus is the durable convergence state of one business record projection.
const ( BindingProjectionPending BindingProjectionStatus = "pending" BindingProjectionProcessing BindingProjectionStatus = "processing" BindingProjectionApplied BindingProjectionStatus = "applied" BindingProjectionFailed BindingProjectionStatus = "failed" )
type BindingTrigger ¶ added in v0.37.0
type BindingTrigger string
BindingTrigger identifies the lifecycle moment associated with a failed business projection. Projection correctness does not depend on triggers: every write applies the latest full desired state. The trigger remains part of InstanceBindingFailedEvent so operators can identify the action that produced that desired state.
const ( BindingTriggerStarted BindingTrigger = "started" BindingTriggerCompleted BindingTrigger = "completed" BindingTriggerReturned BindingTrigger = "returned" BindingTriggerWithdrawn BindingTrigger = "withdrawn" BindingTriggerResubmitted BindingTrigger = "resubmitted" )
type BusinessBindingConfig ¶ added in v0.38.0
type BusinessBindingConfig struct {
TableName string `json:"tableName"`
KeyColumns []string `json:"keyColumns"`
StatusColumn string `json:"statusColumn"`
// InstanceIDColumn is mandatory for business bindings. The projector uses
// it as a compare-and-set fence so a stale instance cannot overwrite the
// state owned by a newer approval round.
InstanceIDColumn *string `json:"instanceIdColumn,omitempty"`
StartedAtColumn *string `json:"startedAtColumn,omitempty"`
FinishedAtColumn *string `json:"finishedAtColumn,omitempty"`
// StatusMapping translates approval instance statuses into host business
// status values. Missing entries fall back to the InstanceStatus string.
StatusMapping map[InstanceStatus]string `json:"statusMapping,omitempty"`
}
BusinessBindingConfig describes the business row targeted by a flow and the columns that receive approval lifecycle state. KeyColumns must exactly match a non-null primary or unique key on TableName.
type BusinessProjection ¶ added in v0.38.0
type BusinessProjection struct {
orm.BaseModel `bun:"table:apv_business_projection,alias:abp"`
orm.FullAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
FlowID string `json:"flowId" bun:"flow_id"`
FlowVersionID string `json:"flowVersionId" bun:"flow_version_id"`
OwnerInstanceID string `json:"ownerInstanceId" bun:"owner_instance_id"`
AppliedOwnerInstanceID *string `json:"appliedOwnerInstanceId,omitempty" bun:"applied_owner_instance_id,nullzero"`
TargetHash string `json:"targetHash" bun:"target_hash"`
Consistency config.ApprovalBindingConsistency `json:"consistency" bun:"consistency"`
Binding *BusinessBindingConfig `json:"binding" bun:"binding,type:jsonb"`
RecordKey json.RawMessage `json:"recordKey" bun:"record_key,type:jsonb"`
DesiredStatus InstanceStatus `json:"desiredStatus" bun:"desired_status"`
DesiredStartedAt timex.DateTime `json:"desiredStartedAt" bun:"desired_started_at"`
DesiredFinishedAt *timex.DateTime `json:"desiredFinishedAt,omitempty" bun:"desired_finished_at,nullzero"`
DesiredRevision int64 `json:"desiredRevision" bun:"desired_revision"`
AppliedRevision int64 `json:"appliedRevision" bun:"applied_revision"`
Status BindingProjectionStatus `json:"status" bun:"status"`
AttemptCount int `json:"attemptCount" bun:"attempt_count"`
NextAttemptAt *timex.DateTime `json:"nextAttemptAt,omitempty" bun:"next_attempt_at,nullzero"`
LeaseUntil *timex.DateTime `json:"leaseUntil,omitempty" bun:"lease_until,nullzero"`
LastError *string `json:"lastError,omitempty" bun:"last_error,nullzero"`
AppliedAt *timex.DateTime `json:"appliedAt,omitempty" bun:"applied_at,nullzero"`
}
BusinessProjection stores the latest desired business-table state for one bound record. One row represents one physical target; OwnerInstanceID and AppliedOwnerInstanceID fence stale instances when a later approval takes ownership.
type BusinessRecordKey ¶ added in v0.38.0
BusinessRecordKey maps every configured key column to the value resolved from an instance's opaque BusinessRef.
type BusinessRefProvider ¶ added in v0.35.0
type BusinessRefProvider interface {
// OnInstanceCreated resolves or creates the business row bound to the
// instance and returns its reference (see Instance.BusinessRef for the
// shape contract). Returning an empty string indicates the host has
// nothing to bind — e.g. the caller already supplied businessRef in the
// start parameters. Runs inside the start_instance transaction; an
// error rolls back instance creation.
OnInstanceCreated(ctx context.Context, db orm.DB, flow *Flow, instance *Instance) (businessRef string, err error)
}
BusinessRefProvider supplies the business reference for a newly created instance when Flow.BindingMode is BindingBusiness. It exists for the one binding step the engine cannot do generically — resolving or allocating the business row — and runs inside the start_instance transaction, so returning an error rolls back the entire instance creation.
The write-back of approval outcomes onto the business table is NOT part of this contract: it is engine-owned, configuration-driven, and not overridable. Hosts needing deeper integration register an InstanceLifecycleHook or subscribe to instance events — extensions add what the engine does not do; they do not replace what it does.
Hosts register an implementation via vef.SupplyBusinessRefProvider.
type BusinessRefResolver ¶ added in v0.35.0
type BusinessRefResolver interface {
// ResolveRecordKey extracts the configured business record key from
// businessRef. Returning an error fails the write-back for this instance.
ResolveRecordKey(ctx context.Context, flow *Flow, businessRef string) (BusinessRecordKey, error)
}
BusinessRefResolver turns the opaque Instance.BusinessRef into the record key the engine-owned write-back matches against. The returned key must name exactly the flow's configured BusinessBinding.KeyColumns. The default resolver treats a single-column ref verbatim and decodes a multi-column ref from a JSON object. Hosts with another ref shape register a custom resolver.
Hosts register an implementation via vef.SupplyBusinessRefResolver.
type CCDefinition ¶
type CCDefinition struct {
Kind CCKind `json:"kind"`
IDs []string `json:"ids,omitempty"`
FormField *string `json:"formField,omitempty"`
Timing CCTiming `json:"timing,omitempty"`
}
CCDefinition represents a CC recipient in node data.
type CCNodeData ¶
type CCNodeData struct {
BaseNodeData
CCs []CCDefinition `json:"ccs,omitempty"`
IsReadConfirmRequired bool `json:"isReadConfirmRequired,omitempty"`
FieldPermissions map[string]Permission `json:"fieldPermissions,omitempty"`
}
CCNodeData contains data specific to CC nodes.
func (*CCNodeData) ApplyTo ¶
func (d *CCNodeData) ApplyTo(node *FlowNode)
ApplyTo applies CC node data to a FlowNode.
func (*CCNodeData) GetCCs ¶
func (d *CCNodeData) GetCCs() []CCDefinition
GetCCs returns the CC definitions from CCNodeData.
type CCNotifiedEvent ¶
type CCNotifiedEvent struct {
InstanceEventBase
NodeID string `json:"nodeId"`
NodeName string `json:"nodeName"`
Recipients []UserInfo `json:"recipients"`
IsManual bool `json:"isManual"`
}
CCNotifiedEvent fired when CC recipients are notified — automatically when a node's CC timing triggers, or manually when a participant adds CCs.
func NewCCNotifiedEvent ¶
func NewCCNotifiedEvent(instance *Instance, node *FlowNode, recipients []UserInfo, isManual bool) *CCNotifiedEvent
func (*CCNotifiedEvent) EventType ¶ added in v0.24.0
func (*CCNotifiedEvent) EventType() string
type CCRecipient ¶ added in v0.35.0
type CCRecipient struct {
User UserInfo `json:"user"`
ReadAt *timex.DateTime `json:"readAt,omitempty"`
}
CCRecipient is one carbon-copy recipient at a node, with the read receipt when the recipient has confirmed reading. Shared by the timeline and the flow-graph projections.
type CCRecord ¶
type CCRecord struct {
orm.BaseModel `bun:"table:apv_cc_record,alias:acr"`
orm.Model
orm.CreationTrackedModel
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID *string `json:"nodeId" bun:"node_id,nullzero"`
// VisitID scopes the record to one node traversal: a rollback redo gets
// its own notification and read-confirm cycle instead of being silently
// satisfied by a prior round's records. Nil only for instance-level
// records that are not anchored to a node.
VisitID *string `json:"visitId" bun:"visit_id,nullzero"`
TaskID *string `json:"taskId" bun:"task_id,nullzero"`
CCUserID string `json:"ccUserId" bun:"cc_user_id"`
CCUserName string `json:"ccUserName" bun:"cc_user_name"`
CCUserDepartmentID *string `json:"ccUserDepartmentId" bun:"cc_user_department_id,nullzero"`
CCUserDepartmentName *string `json:"ccUserDepartmentName" bun:"cc_user_department_name,nullzero"`
IsManual bool `json:"isManual" bun:"is_manual"`
ReadAt *timex.DateTime `json:"readAt" bun:"read_at,nullzero"`
}
CCRecord represents a CC notification record.
func (*CCRecord) Recipient ¶ added in v0.35.0
func (r *CCRecord) Recipient() CCRecipient
Recipient returns the record as a timeline CC recipient: the person snapshot plus the read receipt.
type CallerContext ¶ added in v0.24.0
type CallerContext struct {
// TenantID is the caller's resolved tenant. Empty for super-admin or
// for system-internal callers; must be non-empty for every authenticated
// API request.
TenantID string
// IsSuperAdmin grants cross-tenant access regardless of TenantID.
IsSuperAdmin bool
// IsSystemInternal marks a caller without an HTTP / RPC principal whose
// scope is established by other means, so Authorize passes
// unconditionally. Resource paths must NEVER populate this. In the
// current tree only test fixtures set it (the in-tree system paths —
// timeout scanner, projection worker — act directly on already-loaded,
// trusted rows and never construct a CallerContext); it remains the
// intended marker for any host or future in-process system code that
// legitimately needs to bypass tenant scoping.
IsSystemInternal bool
}
CallerContext bundles the tenant authority of a single API call. Resource handlers resolve it from the security principal (via PrincipalTenantResolver + IsSuperAdmin) and pass it into commands / queries through their struct fields so handlers can enforce data ownership without re-parsing principal details.
Production code MUST populate exactly one of TenantID / IsSuperAdmin / IsSystemInternal. A zero-value CallerContext is treated as **unauthorized** so a forgotten resolver call surfaces as a deny rather than silently waving the request through — this is the deliberate fail- closed posture introduced after the security audit caught fail-open regressions in the default principal resolver.
func (CallerContext) Allows ¶ added in v0.24.0
func (c CallerContext) Allows(entityTenantID string) bool
Allows is the bool variant of Authorize. Query handlers use it when a failed authorization must mimic "not found" rather than surface an error — the typical multi-tenant pattern that avoids leaking entity existence across tenants.
func (CallerContext) Authorize ¶ added in v0.24.0
func (c CallerContext) Authorize(entityTenantID string) error
Authorize reports whether the caller is allowed to act on an entity owned by entityTenantID. Super-admin callers always pass; system-internal callers pass too (no HTTP principal exists, scope is enforced upstream). Every other caller must have a non-empty TenantID that matches the entity tenant exactly — zero-value contexts are rejected.
func (CallerContext) ResolveWriteTenant ¶ added in v0.29.0
func (c CallerContext) ResolveWriteTenant(clientTenant string) (string, error)
ResolveWriteTenant returns the tenant a new or mutated entity must be stamped with. Super-admin / system-internal callers may target any tenant, so the client-supplied value is honored verbatim; every other caller is pinned to their own tenant and fails closed with ErrCrossTenantAccess when they have none — preventing a non-privileged caller from writing into another tenant.
func (CallerContext) TenantScopeFilter ¶ added in v0.29.0
func (c CallerContext) TenantScopeFilter(override string) (*string, error)
TenantScopeFilter resolves the tenant filter for a LIST / metrics query, applying the same fail-closed posture as Authorize. A non-nil result scopes the query to that tenant; a nil result means unfiltered cross-tenant access and is only ever returned to super-admin / system-internal callers. An ordinary caller that resolves to an empty tenant fails closed with ErrCrossTenantAccess, so a forgotten or empty PrincipalTenantResolver result can never silently widen a list query to every tenant's rows — mirroring Authorize's rejection of a zero-value context. The override is honored only for privileged callers; ordinary callers are always pinned to their own tenant regardless of what the client sent. This is the single source of truth for list queries — the resource layer must never read params.TenantID directly.
type ColumnDataType ¶ added in v0.32.0
type ColumnDataType string
ColumnDataType is the dialect-independent logical column type a form field materializes into when the flow version's StorageMode is StorageTable. The form designer infers it from the widget (and lets the user override the few ambiguous ones); the storage layer maps it to a concrete SQL type per dialect.
It is deliberately separate from FieldKind: Kind stays the coarse bucket the field-permission matrix keys off, while a field's physical storage type is an orthogonal concern. An empty ColumnType falls back to a Kind-derived type.
const ( ColumnString ColumnDataType = "string" // short text → VARCHAR(maxLength), TEXT without one ColumnText ColumnDataType = "text" // long/free text → TEXT ColumnInteger ColumnDataType = "integer" // whole number → BIGINT ColumnDecimal ColumnDataType = "decimal" // fixed-point number → NUMERIC/DECIMAL(38, scale) ColumnBoolean ColumnDataType = "boolean" // true/false → BOOLEAN ColumnDate ColumnDataType = "date" // calendar date → DATE ColumnDatetime ColumnDataType = "datetime" // date + time → TIMESTAMP/DATETIME ColumnJSON ColumnDataType = "json" // array/composite → JSONB/JSON )
func (ColumnDataType) IsValid ¶ added in v0.32.0
func (c ColumnDataType) IsValid() bool
IsValid reports whether the column data type is one of the defined values.
type Condition ¶
type Condition struct {
Kind ConditionKind `json:"kind"`
Subject string `json:"subject"`
// Aggregate, when set, evaluates the condition over a detail-table
// field's rows instead of a scalar subject: Subject names the table
// field, Column the numeric column to fold (sum / avg; count works on
// rows and must leave Column empty). Structured on purpose — no string
// DSL to parse, and the designer offers it as table → column → aggregate.
Aggregate AggregateKind `json:"aggregate,omitempty"`
Column string `json:"column,omitempty"`
Operator ConditionOperator `json:"operator"`
Value any `json:"value"`
Expression string `json:"expression"`
}
Condition represents a branch condition evaluated by condition nodes.
type ConditionBranch ¶
type ConditionBranch struct {
ID string `json:"id"`
Label string `json:"label"`
ConditionGroups []ConditionGroup `json:"conditionGroups,omitempty"`
IsDefault bool `json:"isDefault,omitempty"`
Priority int `json:"priority"`
}
ConditionBranch represents a branch in a condition node. Each branch has its own condition groups and can be linked to an edge via its ID.
type ConditionEvaluator ¶
type ConditionEvaluator interface {
// Kind returns the condition kind this evaluator handles.
Kind() ConditionKind
// Evaluate evaluates a single condition against the given evaluation context.
Evaluate(ctx context.Context, cond Condition, ec *EvaluationContext) (bool, error)
}
ConditionEvaluator evaluates branch conditions.
type ConditionGroup ¶
type ConditionGroup struct {
Conditions []Condition `json:"conditions"`
}
ConditionGroup represents a group of conditions evaluated with AND logic. Multiple groups in a branch are evaluated with OR logic.
type ConditionKind ¶
type ConditionKind string
ConditionKind represents the kind of condition for condition branches.
const ( ConditionField ConditionKind = "field" // Field-based condition ConditionExpression ConditionKind = "expression" // Expression-based condition )
type ConditionNodeData ¶
type ConditionNodeData struct {
BaseNodeData
Branches []ConditionBranch `json:"branches,omitempty"`
}
ConditionNodeData contains data specific to condition nodes.
func (*ConditionNodeData) ApplyTo ¶
func (d *ConditionNodeData) ApplyTo(node *FlowNode)
ApplyTo applies condition node data to a FlowNode.
func (*ConditionNodeData) Kind ¶
func (*ConditionNodeData) Kind() NodeKind
Kind returns the node kind.
type ConditionOperator ¶ added in v0.29.0
type ConditionOperator string
ConditionOperator enumerates the comparison operators a field condition may use. The set is the shared contract between the flow designer (which offers operators per field kind) and the engine's field-condition evaluator; both sides must stay in lockstep with this list.
const ( OperatorEquals ConditionOperator = "eq" OperatorNotEquals ConditionOperator = "ne" OperatorGreater ConditionOperator = "gt" OperatorGreaterOrEq ConditionOperator = "gte" OperatorLess ConditionOperator = "lt" OperatorLessOrEq ConditionOperator = "lte" OperatorIn ConditionOperator = "in" OperatorNotIn ConditionOperator = "not_in" OperatorContains ConditionOperator = "contains" OperatorNotContains ConditionOperator = "not_contains" OperatorStartsWith ConditionOperator = "starts_with" OperatorEndsWith ConditionOperator = "ends_with" OperatorIsEmpty ConditionOperator = "is_empty" OperatorIsNotEmpty ConditionOperator = "is_not_empty" )
func (ConditionOperator) IsValid ¶ added in v0.29.0
func (o ConditionOperator) IsValid() bool
IsValid reports whether the operator is one of the defined values.
type ConsecutiveApproverAction ¶
type ConsecutiveApproverAction string
ConsecutiveApproverAction represents the action when the same approver appears in consecutive approval nodes and approved in the previous node.
const ( ConsecutiveApproverNone ConsecutiveApproverAction = "none" ConsecutiveApproverAutoPass ConsecutiveApproverAction = "auto_pass" )
func (ConsecutiveApproverAction) IsValid ¶ added in v0.29.0
func (a ConsecutiveApproverAction) IsValid() bool
IsValid reports whether the consecutive-approver action is one of the defined values.
type Delegation ¶
type Delegation struct {
orm.BaseModel `bun:"table:apv_delegation,alias:ad"`
orm.FullAuditedModel
DelegatorID string `json:"delegatorId" bun:"delegator_id"`
DelegateeID string `json:"delegateeId" bun:"delegatee_id"`
FlowCategoryID *string `json:"flowCategoryId" bun:"flow_category_id,nullzero"`
FlowID *string `json:"flowId" bun:"flow_id,nullzero"`
StartTime timex.DateTime `json:"startTime" bun:"start_time"`
EndTime timex.DateTime `json:"endTime" bun:"end_time"`
IsActive bool `json:"isActive" bun:"is_active"`
Reason *string `json:"reason" bun:"reason,nullzero"`
}
Delegation represents an approval delegation.
type DomainEvent ¶
type DomainEvent interface {
// EventType returns the unique event identifier (e.g., "approval.instance.created").
EventType() string
}
DomainEvent is the contract every approval domain event satisfies. EventType matches the framework's event.Event surface so domain events can be published through the event Bus without adaptation. Business time is carried as OccurredTime in the concrete payload and projected onto Envelope.OccurredAt via event.WithOccurredAt at publish time.
Tenant scope: every instance/task/node/cc-level event carries TenantID so subscribers can route on tenancy without re-querying. Flow-level events (created/updated/etc.) already include TenantID on the payload.
type EdgeDefinition ¶
type EdgeDefinition struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
SourceHandle *string `json:"sourceHandle,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
EdgeDefinition represents a connection between nodes.
type EmptyAssigneeAction ¶
type EmptyAssigneeAction string
EmptyAssigneeAction represents the action when no assignee is found.
const ( EmptyAssigneeAutoPass EmptyAssigneeAction = "auto_pass" EmptyAssigneeTransferAdmin EmptyAssigneeAction = "transfer_admin" EmptyAssigneeTransferSuperior EmptyAssigneeAction = "transfer_superior" EmptyAssigneeTransferApplicant EmptyAssigneeAction = "transfer_applicant" EmptyAssigneeTransferSpecified EmptyAssigneeAction = "transfer_specified" )
func (EmptyAssigneeAction) IsValid ¶ added in v0.29.0
func (a EmptyAssigneeAction) IsValid() bool
IsValid reports whether the empty-assignee action is one of the defined values.
type EndNodeData ¶
type EndNodeData struct {
BaseNodeData
}
EndNodeData contains data specific to end nodes.
func (*EndNodeData) ApplyTo ¶
func (d *EndNodeData) ApplyTo(node *FlowNode)
ApplyTo applies end node data to a FlowNode.
type EvaluationContext ¶
type EvaluationContext struct {
FormData FormData
ApplicantID string
ApplicantDepartmentID *string
// Globals carries host-supplied global variables snapshotted at instance
// start (Instance.Globals). Field conditions resolve a subject against
// Globals before form data — the same shadowing the built-in applicant
// subjects apply — and expression conditions see each entry as a top-level
// binding (the built-in formData / applicantId / applicantDepartmentId
// bindings always win a name collision).
Globals map[string]any
}
EvaluationContext provides context for condition evaluation.
type ExecutionType ¶
type ExecutionType string
ExecutionType represents how a task node is executed. It determines whether the node waits for human decisions or resolves itself the moment the instance enters it.
const ( ExecutionManual ExecutionType = "manual" // Manual: creates tasks and waits for assignees to act ExecutionAutoPass ExecutionType = "auto_pass" // AutoPass: the node passes immediately on entry, no tasks are created ExecutionAutoReject ExecutionType = "auto_reject" // AutoReject: the instance is rejected immediately on entry )
func (ExecutionType) IsValid ¶ added in v0.29.0
func (t ExecutionType) IsValid() bool
IsValid reports whether the execution type is one of the defined values.
type FieldKind ¶
type FieldKind string
FieldKind represents the kind of a form field.
const ( FieldInput FieldKind = "input" FieldTextarea FieldKind = "textarea" FieldSelect FieldKind = "select" FieldNumber FieldKind = "number" FieldDate FieldKind = "date" FieldUpload FieldKind = "upload" // FieldTable is a single-level detail table: its value is a list of rows, // each row an object keyed by the field's Columns. Columns must not nest // another table — approval forms are applications, not data models; deep // structures belong to business tables reached via the business binding. FieldTable FieldKind = "table" )
type FieldOption ¶
FieldOption represents a selectable option for select/radio/checkbox fields.
type Flow ¶
type Flow struct {
orm.BaseModel `bun:"table:apv_flow,alias:af"`
orm.FullAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
CategoryID string `json:"categoryId" bun:"category_id"`
Code string `json:"code" bun:"code"`
Name string `json:"name" bun:"name"`
Icon *string `json:"icon" bun:"icon,nullzero"`
Description *string `json:"description" bun:"description,nullzero"`
BindingMode BindingMode `json:"bindingMode" bun:"binding_mode"`
BusinessBinding *BusinessBindingConfig `json:"businessBinding,omitempty" bun:"business_binding,type:jsonb,nullzero"`
AdminUserIDs []string `json:"adminUserIds" bun:"admin_user_ids,type:jsonb"`
IsAllInitiationAllowed bool `json:"isAllInitiationAllowed" bun:"is_all_initiation_allowed"`
InstanceTitleTemplate string `json:"instanceTitleTemplate" bun:"instance_title_template"`
IsActive bool `json:"isActive" bun:"is_active"`
CurrentVersion int `json:"currentVersion" bun:"current_version"`
}
Flow represents a flow definition.
type FlowCategory ¶
type FlowCategory struct {
orm.BaseModel `bun:"table:apv_flow_category,alias:afc"`
orm.FullAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
Code string `json:"code" bun:"code"`
Name string `json:"name" bun:"name"`
Icon *string `json:"icon" bun:"icon,nullzero"`
ParentID *string `json:"parentId" bun:"parent_id,nullzero"`
SortOrder int `json:"sortOrder" bun:"sort_order"`
IsActive bool `json:"isActive" bun:"is_active"`
Remark *string `json:"remark" bun:"remark,nullzero"`
Children []FlowCategory `json:"children,omitempty" bun:"-"`
}
FlowCategory represents a category for grouping flows.
type FlowCreatedEvent ¶ added in v0.24.0
type FlowCreatedEvent struct {
FlowEventBase
CategoryID string `json:"categoryId"`
}
FlowCreatedEvent fires when a new flow definition is created.
func NewFlowCreatedEvent ¶ added in v0.24.0
func NewFlowCreatedEvent(flow *Flow) *FlowCreatedEvent
func (*FlowCreatedEvent) EventType ¶ added in v0.24.0
func (*FlowCreatedEvent) EventType() string
type FlowDefinition ¶
type FlowDefinition struct {
Nodes []NodeDefinition `json:"nodes"`
Edges []EdgeDefinition `json:"edges"`
}
FlowDefinition represents the structure of a flow definition JSON (React Flow compatible).
type FlowDeployedEvent ¶ added in v0.24.0
type FlowDeployedEvent struct {
FlowEventBase
VersionID string `json:"versionId"`
Version int `json:"version"`
}
FlowDeployedEvent fires when a flow version is deployed.
func NewFlowDeployedEvent ¶ added in v0.24.0
func NewFlowDeployedEvent(flow *Flow, versionID string, version int) *FlowDeployedEvent
func (*FlowDeployedEvent) EventType ¶ added in v0.24.0
func (*FlowDeployedEvent) EventType() string
type FlowEdge ¶
type FlowEdge struct {
orm.BaseModel `bun:"table:apv_flow_edge,alias:afe"`
orm.Model
FlowVersionID string `json:"flowVersionId" bun:"flow_version_id"`
Key string `json:"key" bun:"key,nullzero"`
SourceNodeID string `json:"sourceNodeId" bun:"source_node_id"`
SourceNodeKey string `json:"sourceNodeKey" bun:"source_node_key"`
TargetNodeID string `json:"targetNodeId" bun:"target_node_id"`
TargetNodeKey string `json:"targetNodeKey" bun:"target_node_key"`
SourceHandle *string `json:"sourceHandle" bun:"source_handle,nullzero"`
}
FlowEdge represents a directed edge between two flow nodes.
type FlowEventBase ¶ added in v0.35.0
type FlowEventBase struct {
FlowID string `json:"flowId"`
TenantID string `json:"tenantId"`
Code string `json:"code"`
Name string `json:"name"`
OccurredTime timex.DateTime `json:"occurredTime"`
}
FlowEventBase is the shared envelope of flow-definition events. Code and Name ride along so subscribers can route and render without loading the flow row.
func NewFlowEventBase ¶ added in v0.35.0
func NewFlowEventBase(flow *Flow) FlowEventBase
NewFlowEventBase snapshots the flow into the shared event envelope.
type FlowGraphEdge ¶ added in v0.33.0
type FlowGraphEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
SourceHandle *string `json:"sourceHandle,omitempty"`
}
FlowGraphEdge is one React Flow edge connecting two nodes by their ids.
type FlowGraphNode ¶ added in v0.33.0
type FlowGraphNode struct {
ID string `json:"id"`
NodeID string `json:"nodeId"`
Kind NodeKind `json:"kind"`
Position Position `json:"position"`
Data FlowGraphNodeData `json:"data"`
}
FlowGraphNode is one React Flow node. ID is the React Flow identity — the design-time node key that Position and Edges reference. NodeID is the node's persistent flow-node id: the value action-log nodeId / rollbackToNodeId carry and the process_task rollback API expects as targetNodeId, so a client can map those references onto this graph and drive a rollback without a second lookup.
type FlowGraphNodeData ¶ added in v0.33.0
type FlowGraphNodeData struct {
Name string `json:"name"`
Status NodeProgressStatus `json:"status"`
ExecutionType string `json:"executionType,omitempty"`
ApprovalMethod string `json:"approvalMethod,omitempty"`
PassRule string `json:"passRule,omitempty"`
PassRatio *decimal.Decimal `json:"passRatio,omitempty"`
Participants []NodeParticipant `json:"participants,omitempty"`
CCRecipients []CCRecipient `json:"ccRecipients,omitempty"`
Activities []Activity `json:"activities,omitempty"`
StartedAt *timex.DateTime `json:"startedAt,omitempty"`
FinishedAt *timex.DateTime `json:"finishedAt,omitempty"`
}
FlowGraphNodeData is the payload inside a React Flow node's `data` extension point — the only part of the graph that is ours to shape; the surrounding node/edge structure stays React Flow's contract verbatim. It mirrors the timeline entry of the same node — the node's label, its approval semantics (populated only for approval nodes — handle nodes claim-and-do, they do not decide), its progress status, and everything that happened there: participants, CC recipients, and side-action activities (including the submit/resubmit on the start node), each aggregated across the node's visits in traversal order so a rollback → redo history stays round-by-round. StartedAt/FinishedAt span from the first entry into the node to its latest conclusion (FinishedAt is nil while the node is executing). Enum-derived fields are emitted as strings to match the rest of the detail response.
type FlowInitiator ¶
type FlowInitiator struct {
orm.BaseModel `bun:"table:apv_flow_initiator,alias:afi"`
orm.Model
FlowID string `json:"flowId" bun:"flow_id"`
Kind InitiatorKind `json:"kind" bun:"kind"`
IDs []string `json:"ids" bun:"ids,type:jsonb"`
}
FlowInitiator represents a flow initiator configuration.
type FlowNode ¶
type FlowNode struct {
orm.BaseModel `bun:"table:apv_flow_node,alias:afn"`
orm.FullAuditedModel
FlowVersionID string `json:"flowVersionId" bun:"flow_version_id"`
Key string `json:"key" bun:"key"`
Kind NodeKind `json:"kind" bun:"kind"`
Name string `json:"name" bun:"name"`
Description *string `json:"description" bun:"description,nullzero"`
ExecutionType ExecutionType `json:"executionType" bun:"execution_type"`
ApprovalMethod ApprovalMethod `json:"approvalMethod" bun:"approval_method"`
PassRule PassRule `json:"passRule" bun:"pass_rule"`
PassRatio decimal.Decimal `json:"passRatio" bun:"pass_ratio,type:numeric(5,2)"`
EmptyAssigneeAction EmptyAssigneeAction `json:"emptyAssigneeAction" bun:"empty_assignee_action"`
FallbackUserIDs []string `json:"fallbackUserIds" bun:"fallback_user_ids,type:jsonb"`
AdminUserIDs []string `json:"adminUserIds" bun:"admin_user_ids,type:jsonb"`
SameApplicantAction SameApplicantAction `json:"sameApplicantAction" bun:"same_applicant_action"`
IsRollbackAllowed bool `json:"isRollbackAllowed" bun:"is_rollback_allowed"`
RollbackType RollbackType `json:"rollbackType" bun:"rollback_type"`
RollbackDataStrategy RollbackDataStrategy `json:"rollbackDataStrategy" bun:"rollback_data_strategy"`
RollbackTargetKeys []string `json:"rollbackTargetKeys" bun:"rollback_target_keys,type:jsonb,nullzero"`
IsAddAssigneeAllowed bool `json:"isAddAssigneeAllowed" bun:"is_add_assignee_allowed"`
AddAssigneeTypes []AddAssigneeType `json:"addAssigneeTypes" bun:"add_assignee_types,type:jsonb"`
IsRemoveAssigneeAllowed bool `json:"isRemoveAssigneeAllowed" bun:"is_remove_assignee_allowed"`
FieldPermissions map[string]Permission `json:"fieldPermissions" bun:"field_permissions,type:jsonb"`
IsManualCCAllowed bool `json:"isManualCcAllowed" bun:"is_manual_cc_allowed"`
IsTransferAllowed bool `json:"isTransferAllowed" bun:"is_transfer_allowed"`
IsOpinionRequired bool `json:"isOpinionRequired" bun:"is_opinion_required"`
TimeoutHours int `json:"timeoutHours" bun:"timeout_hours"`
TimeoutAction TimeoutAction `json:"timeoutAction" bun:"timeout_action"`
TimeoutNotifyBeforeHours int `json:"timeoutNotifyBeforeHours" bun:"timeout_notify_before_hours"`
UrgeCooldownMinutes int `json:"urgeCooldownMinutes" bun:"urge_cooldown_minutes"`
ConsecutiveApproverAction ConsecutiveApproverAction `json:"consecutiveApproverAction" bun:"consecutive_approver_action"`
IsReadConfirmRequired bool `json:"isReadConfirmRequired" bun:"is_read_confirm_required"`
Branches []ConditionBranch `json:"branches" bun:"branches,type:jsonb,nullzero"`
}
FlowNode represents a node within a flow version.
type FlowNodeAssignee ¶
type FlowNodeAssignee struct {
orm.BaseModel `bun:"table:apv_flow_node_assignee,alias:afna"`
orm.Model
NodeID string `json:"nodeId" bun:"node_id"`
Kind AssigneeKind `json:"kind" bun:"kind"`
IDs []string `json:"ids" bun:"ids,type:jsonb"`
FormField *string `json:"formField" bun:"form_field,nullzero"`
SortOrder int `json:"sortOrder" bun:"sort_order"`
}
FlowNodeAssignee represents a node assignee configuration.
type FlowNodeCC ¶
type FlowNodeCC struct {
orm.BaseModel `bun:"table:apv_flow_node_cc,alias:afnc"`
orm.Model
NodeID string `json:"nodeId" bun:"node_id"`
Kind CCKind `json:"kind" bun:"kind"`
IDs []string `json:"ids" bun:"ids,type:jsonb"`
FormField *string `json:"formField" bun:"form_field,nullzero"`
Timing CCTiming `json:"timing" bun:"timing"`
}
FlowNodeCC represents a node CC configuration.
type FlowPublishedEvent ¶
type FlowPublishedEvent struct {
FlowEventBase
VersionID string `json:"versionId"`
}
FlowPublishedEvent fires when a flow version is published.
func NewFlowPublishedEvent ¶
func NewFlowPublishedEvent(flow *Flow, versionID string) *FlowPublishedEvent
func (*FlowPublishedEvent) EventType ¶ added in v0.24.0
func (*FlowPublishedEvent) EventType() string
type FlowToggledEvent ¶ added in v0.24.0
type FlowToggledEvent struct {
FlowEventBase
IsActive bool `json:"isActive"`
}
FlowToggledEvent fires when a flow is activated or deactivated.
func NewFlowToggledEvent ¶ added in v0.24.0
func NewFlowToggledEvent(flow *Flow, isActive bool) *FlowToggledEvent
func (*FlowToggledEvent) EventType ¶ added in v0.24.0
func (*FlowToggledEvent) EventType() string
type FlowUpdatedEvent ¶ added in v0.24.0
type FlowUpdatedEvent struct {
FlowEventBase
}
FlowUpdatedEvent fires when a flow definition's metadata is updated.
func NewFlowUpdatedEvent ¶ added in v0.24.0
func NewFlowUpdatedEvent(flow *Flow) *FlowUpdatedEvent
func (*FlowUpdatedEvent) EventType ¶ added in v0.24.0
func (*FlowUpdatedEvent) EventType() string
type FlowVersion ¶
type FlowVersion struct {
orm.BaseModel `bun:"table:apv_flow_version,alias:afv"`
orm.FullAuditedModel
FlowID string `json:"flowId" bun:"flow_id"`
Version int `json:"version" bun:"version"`
Status VersionStatus `json:"status" bun:"status"`
Description *string `json:"description" bun:"description,nullzero"`
StorageMode StorageMode `json:"storageMode" bun:"storage_mode"`
FlowSchema *FlowDefinition `json:"flowSchema" bun:"flow_schema,type:jsonb,nullzero"`
// FormSchema is the host-owned form designer document, stored and returned
// as semantically equal JSON: the jsonb column normalizes formatting and
// key order, while numeric precision is preserved end-to-end. The
// framework never interprets it (see FormSchemaParser).
FormSchema json.RawMessage `json:"formSchema" bun:"form_schema,type:jsonb,nullzero"`
// FormFields is the flat field list derived from FormSchema at deploy —
// the only form shape the framework itself consumes.
FormFields []FormFieldDefinition `json:"formFields" bun:"form_fields,type:jsonb,nullzero"`
PublishedAt *timex.DateTime `json:"publishedAt" bun:"published_at,nullzero"`
PublishedBy *string `json:"publishedBy" bun:"published_by,nullzero"`
// BusinessBinding is the immutable binding snapshot captured when the
// version is deployed. Runtime instances never read the mutable Flow copy.
BusinessBinding *BusinessBindingConfig `json:"businessBinding,omitempty" bun:"business_binding,type:jsonb,nullzero"`
}
FlowVersion represents a versioned snapshot of a flow definition.
type FormData ¶
FormData wraps a map to provide helper methods for form data operations.
func NewFormData ¶
func (FormData) Clone ¶
Clone creates a deep copy via JSON serialization. It returns an error if the map contains values that cannot be marshaled to JSON (e.g. channels, functions). The caller should treat an error as uncloneable data and act accordingly rather than silently losing the original content.
type FormFieldDefinition ¶
type FormFieldDefinition struct {
// Key is the unique identifier for this field (used in form data keys).
Key string `json:"key"`
// Kind is the field type (e.g., "input", "textarea", "select", "number", "date", "upload", "table").
Kind FieldKind `json:"kind"`
// Label is the display label.
Label string `json:"label"`
// Placeholder is the input placeholder text.
Placeholder string `json:"placeholder,omitempty"`
// DefaultValue is the default value for this field.
DefaultValue any `json:"defaultValue,omitempty"`
// IsRequired indicates whether this field is required.
IsRequired bool `json:"isRequired,omitempty"`
// Options is the list of selectable options (for select, radio, checkbox, etc.).
Options []FieldOption `json:"options,omitempty"`
// Validation contains validation rules.
Validation *ValidationRule `json:"validation,omitempty"`
// Props contains additional component-specific properties.
Props map[string]any `json:"props,omitempty"`
// SortOrder controls the display order.
SortOrder int `json:"sortOrder"`
// ColumnType is the dialect-independent logical column type used when the
// flow version's StorageMode is StorageTable. Empty falls back to a coarse
// type derived from Kind (back-compat with schemas authored before this field).
ColumnType ColumnDataType `json:"columnType,omitempty"`
// Scale is the number of fractional digits for a ColumnDecimal column (the
// DECIMAL/NUMERIC scale). Nil means an integer-shaped decimal (scale 0).
Scale *int `json:"scale,omitempty"`
// Columns defines the row shape of a table field (Kind == FieldTable):
// each column is itself a field definition and reuses the same kinds and
// validation rules, except that a column must not be another table —
// detail tables are single-level by design. For a table field itself,
// Validation.MinLength / MaxLength bound the ROW COUNT, and IsRequired
// means "at least one row".
Columns []FormFieldDefinition `json:"columns,omitempty"`
}
FormFieldDefinition represents a single form field.
type FormSchemaParser ¶ added in v0.38.0
type FormSchemaParser interface {
// ParseFormFields extracts the flat field definitions from the raw form
// schema document. A nil or empty schema yields (nil, nil) — a flow
// without a form. Errors abort the deploy.
//
// ctx carries the deploy request's deadline and cancellation. The built-in
// parser is pure and ignores it, but a host parser may perform I/O (resolving
// remote option sources, calling an external validation service) while
// deriving fields, and must honor ctx cancellation on those paths.
ParseFormFields(ctx context.Context, schema json.RawMessage) ([]FormFieldDefinition, error)
}
FormSchemaParser derives the flat field list the framework consumes from the host-owned form schema document. The framework stores and returns the schema verbatim and never interprets it; only the parsed fields enter form validation, storage-table DDL, aggregate checks, and field-permission resolution. Parsing runs once at flow deploy; runtime paths read the persisted fields, so parser upgrades never affect already-deployed versions.
type FormSnapshot ¶
type FormSnapshot struct {
orm.BaseModel `bun:"table:apv_form_snapshot,alias:afs"`
orm.Model
orm.CreationTrackedModel
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID string `json:"nodeId" bun:"node_id"`
FormData map[string]any `json:"formData" bun:"form_data,type:jsonb"`
}
FormSnapshot represents a form snapshot for rollback strategies.
type FormTable ¶ added in v0.32.0
type FormTable struct {
orm.BaseModel `bun:"table:apv_form_table,alias:aft"`
orm.CreationAuditedModel
FlowID string `json:"flowId" bun:"flow_id"`
VersionID string `json:"versionId" bun:"version_id"`
PhysicalTableName string `json:"physicalTableName" bun:"physical_table_name"`
// SourceFieldKey names the detail-table field this child table projects;
// empty for the version's main projection table.
SourceFieldKey string `json:"sourceFieldKey" bun:"source_field_key"`
}
FormTable records the dedicated physical table generated for a published version whose StorageMode is StorageTable. It is the single source of truth for what DDL the framework generated: the engine consults it (idempotency) before creating a table for a version, and operators can map a version to its projection table through it. One row per physical table: the main projection table plus one child table per detail-table field, disambiguated by SourceFieldKey ((version_id, source_field_key) is unique).
type FormTableColumn ¶ added in v0.32.0
type FormTableColumn struct {
orm.BaseModel `bun:"table:apv_form_table_column,alias:aftc"`
orm.CreationAuditedModel
FormTableID string `json:"formTableId" bun:"form_table_id"`
ColumnName string `json:"columnName" bun:"column_name"`
ColumnType string `json:"columnType" bun:"column_type"`
IsNullable bool `json:"isNullable" bun:"is_nullable"`
SourceFieldKey *string `json:"sourceFieldKey" bun:"source_field_key,nullzero"`
SortOrder int `json:"sortOrder" bun:"sort_order"`
}
FormTableColumn records a single generated column of a FormTable. It mirrors the physical column produced from one form field (or the built-in id / instance_id / created_at columns), so the generated schema can be inspected without reflecting on the database catalog.
type HandleNodeData ¶
type HandleNodeData struct {
BaseNodeData
TaskNodeData
}
HandleNodeData contains data specific to handle nodes.
func (*HandleNodeData) ApplyTo ¶
func (d *HandleNodeData) ApplyTo(node *FlowNode)
ApplyTo applies handle node data to a FlowNode, resolving omitted fields to the handle defaults (sequential execution, PassAny rule).
type InitiatorKind ¶
type InitiatorKind string
InitiatorKind represents the kind of initiator.
const ( InitiatorUser InitiatorKind = "user" InitiatorRole InitiatorKind = "role" InitiatorDepartment InitiatorKind = "department" )
func (InitiatorKind) IsValid ¶ added in v0.36.0
func (k InitiatorKind) IsValid() bool
IsValid checks if the InitiatorKind is a valid value.
type Instance ¶
type Instance struct {
orm.BaseModel `bun:"table:apv_instance,alias:ai"`
orm.FullAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
FlowID string `json:"flowId" bun:"flow_id"`
// FlowCode snapshots the flow's business code at instance creation, like
// the applicant fields. Flow codes are immutable (update_flow never
// touches code), so the snapshot cannot drift; carrying it here lets
// every event and projection self-describe without joining apv_flow.
FlowCode string `json:"flowCode" bun:"flow_code"`
FlowVersionID string `json:"flowVersionId" bun:"flow_version_id"`
Title string `json:"title" bun:"title"`
InstanceNo string `json:"instanceNo" bun:"instance_no"`
ApplicantID string `json:"applicantId" bun:"applicant_id"`
ApplicantName string `json:"applicantName" bun:"applicant_name"`
ApplicantDepartmentID *string `json:"applicantDepartmentId" bun:"applicant_department_id,nullzero"`
ApplicantDepartmentName *string `json:"applicantDepartmentName" bun:"applicant_department_name,nullzero"`
Status InstanceStatus `json:"status" bun:"status"`
CurrentNodeID *string `json:"currentNodeId" bun:"current_node_id,nullzero"`
FinishedAt *timex.DateTime `json:"finishedAt" bun:"finished_at,nullzero"`
// BusinessRef is the opaque reference to the bound business record. The
// engine only parses the default single-key / composite-JSON shapes — hosts
// remain free to choose another shape (business number, encoded tuple, …)
// by registering BusinessRefResolver.
BusinessRef *string `json:"businessRef" bun:"business_ref,nullzero"`
FormData map[string]any `json:"formData" bun:"form_data,type:jsonb,nullzero"`
// Globals is the host-supplied global-variable snapshot taken at instance
// start (tenant attributes, applicant roles, business limits, …). Condition
// evaluation resolves field subjects and expression bindings against it, so
// routing stays deterministic across re-evaluation — like the applicant
// department, it reflects the world at initiation, not live state.
Globals map[string]any `json:"globals" bun:"globals,type:jsonb,nullzero"`
// BusinessProjectionID identifies the durable target state claimed at
// instance start. Every later transition writes through that state so flow
// edits and stale workers cannot redirect or overwrite the binding.
BusinessProjectionID *string `json:"businessProjectionId,omitempty" bun:"business_projection_id,nullzero"`
}
Instance represents a flow instance.
type InstanceBindingFailedEvent ¶ added in v0.24.0
type InstanceBindingFailedEvent struct {
InstanceEventBase
// Trigger is the lifecycle moment represented by the failed desired state.
Trigger BindingTrigger `json:"trigger"`
// Status is the instance status the write-back attempted to persist into
// the business status column at that moment.
Status InstanceStatus `json:"status"`
BusinessTable string `json:"businessTable"`
ErrorMessage string `json:"errorMessage"`
}
InstanceBindingFailedEvent fires when an eventual business projection attempt fails after the desired approval state has committed. The durable worker keeps retrying; this event is an operator notification, not the retry mechanism. Synchronous failures roll back the approval transaction and do not emit this event.
func NewInstanceBindingFailedEvent ¶ added in v0.24.0
func NewInstanceBindingFailedEvent(instance *Instance, trigger BindingTrigger, status InstanceStatus, businessTable, errorMessage string) *InstanceBindingFailedEvent
func (*InstanceBindingFailedEvent) EventType ¶ added in v0.24.0
func (*InstanceBindingFailedEvent) EventType() string
type InstanceCompletedEvent ¶
type InstanceCompletedEvent struct {
InstanceEventBase
// FinalStatus is always one of the IsFinal() statuses — approved,
// rejected, or terminated. Paused states (returned / withdrawn) never
// fire this event.
FinalStatus InstanceStatus `json:"finalStatus"`
FinishedAt timex.DateTime `json:"finishedAt"`
// Reason is the administrator's stated reason when FinalStatus is
// terminated; nil for approved / rejected completions, whose deciding
// opinions live on the task events.
Reason *string `json:"reason,omitempty"`
}
InstanceCompletedEvent fired when instance reaches a final status.
func NewInstanceCompletedEvent ¶
func NewInstanceCompletedEvent(instance *Instance, finalStatus InstanceStatus) *InstanceCompletedEvent
NewInstanceCompletedEvent builds the completion event. FinishedAt is taken from the instance (every completion path stamps it before publishing) and falls back to now for defensive completeness.
func (*InstanceCompletedEvent) EventType ¶ added in v0.24.0
func (*InstanceCompletedEvent) EventType() string
type InstanceCreatedEvent ¶
type InstanceCreatedEvent struct {
InstanceEventBase
}
InstanceCreatedEvent fired when a new instance is created.
func NewInstanceCreatedEvent ¶
func NewInstanceCreatedEvent(instance *Instance) *InstanceCreatedEvent
func (*InstanceCreatedEvent) EventType ¶ added in v0.24.0
func (*InstanceCreatedEvent) EventType() string
type InstanceEvent ¶ added in v0.37.0
InstanceEvent is the closed set of instance-scoped domain events — every event type embedding InstanceEventBase satisfies it automatically. It is the type bound of SubscribeInstance.
type InstanceEventBase ¶ added in v0.35.0
type InstanceEventBase struct {
InstanceID string `json:"instanceId"`
InstanceNo string `json:"instanceNo"`
TenantID string `json:"tenantId"`
Title string `json:"title"`
FlowID string `json:"flowId"`
FlowCode string `json:"flowCode"`
BusinessRef *string `json:"businessRef,omitempty"`
Applicant UserInfo `json:"applicant"`
OccurredTime timex.DateTime `json:"occurredTime"`
}
InstanceEventBase is the shared envelope of every instance-scoped domain event. It is sized so a subscriber can act without querying the approval tables: route on TenantID / FlowCode, locate the host's own record through BusinessRef, and render a notification from Title / InstanceNo / Applicant. Fields are snapshots taken when the event fires — they describe the world at that moment, not the current row. Unbounded payloads (form data) stay out deliberately; subscribers that need them fetch by InstanceID.
func NewInstanceEventBase ¶ added in v0.35.0
func NewInstanceEventBase(instance *Instance) InstanceEventBase
NewInstanceEventBase snapshots the instance into the shared event envelope, stamping OccurredTime with the current time.
type InstanceFilter ¶ added in v0.37.0
InstanceFilter is a declarative, data-backed routing filter for instance events and lifecycle hooks. It captures the routing question — "is this instance mine?" — as data rather than an opaque predicate, so the framework can evaluate it anywhere (consumer side today, transport push-down tomorrow) and operators can read a subscription's scope off its registration site.
Semantics: within one filter, an empty dimension is unconstrained and a populated dimension matches when the value is in the list (OR); multiple filters passed to the same subscription must all match (AND). Business predicates (final status, form values, …) deliberately stay out — they belong in the handler.
func ForFlows ¶ added in v0.37.0
func ForFlows(codes ...string) InstanceFilter
ForFlows restricts a subscription (or a filtered lifecycle hook) to instances of the named flow codes.
func ForTenants ¶ added in v0.37.0
func ForTenants(ids ...string) InstanceFilter
ForTenants restricts a subscription (or a filtered lifecycle hook) to instances of the named tenants.
func (InstanceFilter) Matches ¶ added in v0.37.0
func (f InstanceFilter) Matches(flowCode, tenantID string) bool
Matches reports whether an instance identified by flowCode / tenantID passes this filter.
type InstanceFlowGraph ¶ added in v0.33.0
type InstanceFlowGraph struct {
Nodes []FlowGraphNode `json:"nodes"`
Edges []FlowGraphEdge `json:"edges"`
}
InstanceFlowGraph is a React Flow–ready, read-only projection of an instance's flow definition annotated with runtime progress. Its Nodes and Edges map directly onto React Flow's node/edge shape so a client can render it without reshaping — except the node kind, which stays in `kind` (mirroring NodeDefinition's wire format): React Flow's `type` selects the rendering component and belongs to the client, so the graph carries the business discriminator instead. It is distinct from the editor-facing shared.FlowGraph (raw definition rows without progress): this one is pinned to the instance's version and carries per-node progress.
type InstanceGlobalsResolver ¶ added in v0.35.0
type InstanceGlobalsResolver interface {
// Resolve returns the global-variable snapshot for an instance the given
// principal is starting on the given flow. A nil map means "no globals".
Resolve(ctx context.Context, principal *security.Principal, flowCode string) (map[string]any, error)
}
InstanceGlobalsResolver resolves the host-defined global variables for a new instance — attributes the flow's conditions may reference beyond the form data and the built-in applicant subjects (tenant attributes, applicant roles, business limits, …). Resolved SERVER-SIDE from the authenticated principal at instance start and snapshotted onto Instance.Globals: globals participate in routing, so they must never be accepted from the client request body. Implemented by host apps (override via fx.Replace); the default resolver returns no globals.
type InstanceLifecycleHook ¶ added in v0.24.0
type InstanceLifecycleHook interface {
// OnInstanceCreated runs after the instance row is persisted and the
// initial action log is written, but before the engine advances to
// the first node. Returning an error rolls back start_instance.
OnInstanceCreated(ctx context.Context, db orm.DB, instance *Instance) error
// OnInstanceTransition runs inside the same transaction as every
// instance status transition — completion (to.IsFinal()), return,
// withdrawal, resubmission, termination — after the engine-owned
// business projection has recorded (and, in synchronous mode, applied)
// the new state, so the hook observes the business table as the
// transition leaves it. instance.Status already carries to. Returning
// an error rolls back the whole transition. For side effects that need
// no transactional coupling, subscribe to the corresponding instance
// events (or bridge them into commands via BindCommand) instead.
OnInstanceTransition(ctx context.Context, db orm.DB, instance *Instance, from, to InstanceStatus) error
}
InstanceLifecycleHook is a synchronous extension point for host applications that need to react to approval lifecycle moments inside the same transaction as the business change. Unlike event subscriptions — which are asynchronous, retryable through outbox, and fire after commit — hooks run *during* the transaction, so a hook that returns an error aborts the surrounding business operation.
Use hooks for invariants that must hold within the transaction (e.g. allocating a business row, writing a tightly-coupled record). Use event subscriptions for everything else (webhooks, notifications, analytics, async integrations).
Multiple implementations are aggregated via FX group `group:"vef:approval:lifecycle_hooks"`. The invocation order is unspecified — FX value groups carry no ordering — so hooks must be mutually independent; any non-nil error stops the remaining hooks and bubbles back to the caller.
func NewFilteredLifecycleHook ¶ added in v0.37.0
func NewFilteredLifecycleHook(hook InstanceLifecycleHook, filters ...InstanceFilter) InstanceLifecycleHook
NewFilteredLifecycleHook wraps a hook with the same declarative routing filters used by SubscribeInstance, so the hook only sees instances it declared interest in (non-matching instances pass through as a no-op). Hooks are global by default; hosts whose hook serves a single flow wrap it at registration:
vef.ProvideApprovalLifecycleHook(func() approval.InstanceLifecycleHook {
return approval.NewFilteredLifecycleHook(newRightApplicationHook(), approval.ForFlows("right_application"))
})
No filters returns the hook unchanged.
type InstanceNoGenerator ¶
type InstanceNoGenerator interface {
// Generate creates a unique instance number for a flow identified by flowCode.
Generate(ctx context.Context, flowCode string) (string, error)
}
InstanceNoGenerator generates unique instance numbers for flow instances.
type InstanceResubmittedEvent ¶
type InstanceResubmittedEvent struct {
InstanceEventBase
Operator UserInfo `json:"operator"`
}
InstanceResubmittedEvent fired when the initiator resubmits a returned instance.
func NewInstanceResubmittedEvent ¶
func NewInstanceResubmittedEvent(instance *Instance, operator UserInfo) *InstanceResubmittedEvent
func (*InstanceResubmittedEvent) EventType ¶ added in v0.24.0
func (*InstanceResubmittedEvent) EventType() string
type InstanceReturnedEvent ¶
type InstanceReturnedEvent struct {
InstanceEventBase
FromNodeID string `json:"fromNodeId"`
FromNodeName string `json:"fromNodeName"`
ToNodeID string `json:"toNodeId"`
ToNodeName string `json:"toNodeName"`
Operator UserInfo `json:"operator"`
// Opinion is the rollback opinion provided by the operator, if any.
Opinion *string `json:"opinion,omitempty"`
}
InstanceReturnedEvent fired when the flow is sent back to the initiator: the instance pauses as returned until the applicant resubmits or abandons.
func NewInstanceReturnedEvent ¶
func NewInstanceReturnedEvent(instance *Instance, fromNode, toNode *FlowNode, operator UserInfo, opinion *string) *InstanceReturnedEvent
func (*InstanceReturnedEvent) EventType ¶ added in v0.24.0
func (*InstanceReturnedEvent) EventType() string
type InstanceRolledBackEvent ¶
type InstanceRolledBackEvent struct {
InstanceEventBase
FromNodeID string `json:"fromNodeId"`
FromNodeName string `json:"fromNodeName"`
ToNodeID string `json:"toNodeId"`
ToNodeName string `json:"toNodeName"`
Operator UserInfo `json:"operator"`
// Opinion is the rollback opinion provided by the operator, if any.
Opinion *string `json:"opinion,omitempty"`
}
InstanceRolledBackEvent fired when a task decision sends the flow back to an intermediate node (the instance keeps running). Rolling back to the start node fires InstanceReturnedEvent instead.
func NewInstanceRolledBackEvent ¶
func NewInstanceRolledBackEvent(instance *Instance, fromNode, toNode *FlowNode, operator UserInfo, opinion *string) *InstanceRolledBackEvent
func (*InstanceRolledBackEvent) EventType ¶ added in v0.24.0
func (*InstanceRolledBackEvent) EventType() string
type InstanceStatus ¶
type InstanceStatus string
InstanceStatus represents the status of a flow instance.
const ( InstanceRunning InstanceStatus = "running" InstanceApproved InstanceStatus = "approved" InstanceRejected InstanceStatus = "rejected" InstanceWithdrawn InstanceStatus = "withdrawn" InstanceReturned InstanceStatus = "returned" InstanceTerminated InstanceStatus = "terminated" )
func (InstanceStatus) IsFinal ¶
func (s InstanceStatus) IsFinal() bool
func (InstanceStatus) String ¶
func (s InstanceStatus) String() string
type InstanceSubscribeOption ¶ added in v0.37.0
type InstanceSubscribeOption interface {
// contains filtered or unexported methods
}
InstanceSubscribeOption configures a SubscribeInstance call. InstanceFilter values are options themselves, so filters and settings mix in one list.
func WithConcurrency ¶ added in v0.37.0
func WithConcurrency(n int) InstanceSubscribeOption
WithConcurrency sets the per-subscription worker count, forwarded to the underlying event subscription.
func WithGroup ¶ added in v0.37.0
func WithGroup(name string) InstanceSubscribeOption
WithGroup pins the subscription's consumer group explicitly, overriding the derived default. Use it for production-critical subscribers (the name survives refactors) and whenever the handler is an anonymous function.
type InstanceWithdrawnEvent ¶
type InstanceWithdrawnEvent struct {
InstanceEventBase
Operator UserInfo `json:"operator"`
// Reason is the applicant's stated withdrawal reason, when provided.
Reason *string `json:"reason,omitempty"`
}
InstanceWithdrawnEvent fired when applicant withdraws the instance.
func NewInstanceWithdrawnEvent ¶
func NewInstanceWithdrawnEvent(instance *Instance, operator UserInfo, reason *string) *InstanceWithdrawnEvent
func (*InstanceWithdrawnEvent) EventType ¶ added in v0.24.0
func (*InstanceWithdrawnEvent) EventType() string
type NodeAutoPassedEvent ¶
type NodeAutoPassedEvent struct {
InstanceEventBase
NodeID string `json:"nodeId"`
NodeName string `json:"nodeName"`
Reason string `json:"reason"`
}
NodeAutoPassedEvent fired when a node passes without any human decision: auto-pass execution type, empty-assignee auto-pass, or same-applicant auto-pass. Reason carries which rule produced the pass so audit timelines can render the skipped step.
func NewNodeAutoPassedEvent ¶
func NewNodeAutoPassedEvent(instance *Instance, node *FlowNode, reason string) *NodeAutoPassedEvent
func (*NodeAutoPassedEvent) EventType ¶ added in v0.24.0
func (*NodeAutoPassedEvent) EventType() string
type NodeData ¶
type NodeData interface {
// Kind returns the node kind (start, end, approval, handle, cc, condition).
Kind() NodeKind
// GetName returns the display name of the node.
GetName() string
// GetDescription returns the optional description of the node.
GetDescription() *string
// ApplyTo applies this node data's configuration to the given FlowNode,
// resolving omitted optional fields to their documented defaults so the
// persisted node always carries complete, valid configuration.
ApplyTo(node *FlowNode)
}
NodeData is the interface implemented by all node data types.
type NodeDefinition ¶
type NodeDefinition struct {
ID string `json:"id"`
Kind NodeKind `json:"kind"`
Position Position `json:"position"`
Data json.RawMessage `json:"data,omitempty"`
}
NodeDefinition represents a node in the flow definition. The node kind travels in `kind`, deliberately not React Flow's `type`: in React Flow, `type` selects the rendering component and belongs to whichever client renders the graph, so the persisted contract carries the business discriminator and stays decoupled from rendering concerns.
func (*NodeDefinition) ParseData ¶
func (nd *NodeDefinition) ParseData() (NodeData, error)
ParseData parses Data into the appropriate typed struct based on Kind.
type NodeKind ¶
type NodeKind string
NodeKind represents the kind of a flow node. It defines the different types of nodes that can exist in a workflow.
const ( NodeStart NodeKind = "start" // Start node: the entry point of a workflow NodeApproval NodeKind = "approval" // Approval node: requires approval action from assignees NodeHandle NodeKind = "handle" // Handle node: requires processing/handling action from assignees NodeCondition NodeKind = "condition" // Condition node: branches the flow based on conditions NodeEnd NodeKind = "end" // End node: the terminal point of a workflow NodeCC NodeKind = "cc" // CC node: sends notifications to specified users )
type NodeParticipant ¶ added in v0.35.0
type NodeParticipant struct {
TaskID string `json:"taskId"`
User UserInfo `json:"user"`
Delegator *UserInfo `json:"delegator,omitempty"`
Status string `json:"status"`
Deadline *timex.DateTime `json:"deadline,omitempty"`
IsTimeout bool `json:"isTimeout,omitempty"`
Opinion *string `json:"opinion,omitempty"`
Attachments []string `json:"attachments,omitempty"`
ActionTime *timex.DateTime `json:"actionTime,omitempty"`
TransferTo *UserInfo `json:"transferTo,omitempty"`
}
NodeParticipant is one assignee's involvement at an approval/handle node during a single visit: the task identity (TaskID is what task operations are submitted against), the assignee (plus delegator when the task arrived via delegation), the task status verbatim, and — once the task is finished — the outcome details fused from the action log that finished it. IsTimeout marks tasks the timeout scanner decided or escalated. Shared by the timeline and the flow-graph projections so the two views can never disagree about who did what.
type NodeProgressStatus ¶ added in v0.33.0
type NodeProgressStatus string
NodeProgressStatus describes how far an instance has advanced through a flow node, for the read-only progress rendering of the flow graph. Except for pending (a node with no visit yet), the values mirror NodeVisitStatus — a reached node reports the outcome of its latest visit, so the graph and the timeline share one status vocabulary.
const ( // NodeProgressPending marks a node the instance has not reached yet. NodeProgressPending NodeProgressStatus = "pending" // NodeProgressActive marks a node the instance is currently sitting on. NodeProgressActive NodeProgressStatus = "active" // NodeProgressPassed marks a node the instance has already passed. NodeProgressPassed NodeProgressStatus = "passed" // NodeProgressRejected marks the node whose decision rejected the instance. NodeProgressRejected NodeProgressStatus = "rejected" // NodeProgressReturned marks a node from which the flow was sent back. NodeProgressReturned NodeProgressStatus = "returned" // NodeProgressCanceled marks a node whose visit was cut short by a // withdraw or terminate. NodeProgressCanceled NodeProgressStatus = "canceled" )
type NodeVisit ¶ added in v0.35.0
type NodeVisit struct {
orm.BaseModel `bun:"table:apv_node_visit,alias:anv"`
orm.CreationAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID string `json:"nodeId" bun:"node_id"`
Sequence int `json:"sequence" bun:"sequence"`
Status NodeVisitStatus `json:"status" bun:"status"`
FinishedAt *timex.DateTime `json:"finishedAt" bun:"finished_at,nullzero"`
}
NodeVisit records one traversal of a flow node by an instance: the engine inserts an active row when it enters a node and stamps the outcome (passed / rejected / returned / canceled) plus FinishedAt when the node concludes. Sequence is the per-instance step number, so ordering visits by it reconstructs the exact path the instance took — the authoritative source for the instance timeline and flow-graph progress projections. A node re-entered after a rollback gets a fresh visit row.
type NodeVisitStatus ¶ added in v0.35.0
type NodeVisitStatus string
NodeVisitStatus represents the lifecycle status of a node visit — one traversal of a flow node by an instance.
const ( NodeVisitActive NodeVisitStatus = "active" // The instance is currently sitting on the node NodeVisitPassed NodeVisitStatus = "passed" // The node concluded and the flow moved on NodeVisitRejected NodeVisitStatus = "rejected" // The node concluded by rejecting the instance NodeVisitReturned NodeVisitStatus = "returned" // The flow was sent back from this node (rollback) NodeVisitCanceled NodeVisitStatus = "canceled" // The visit was cut short (withdraw / terminate) )
func (NodeVisitStatus) String ¶ added in v0.35.0
func (s NodeVisitStatus) String() string
type PassRule ¶
type PassRule string
PassRule represents the strategy for passing the node (for Parallel/Or methods).
type PassRuleContext ¶
type PassRuleContext struct {
ApprovedCount int
RejectedCount int
TotalCount int
PassRatio float64
}
PassRuleContext provides context for pass rule evaluation.
type PassRuleResult ¶
type PassRuleResult int
PassRuleResult indicates the outcome of pass rule evaluation.
const ( PassRulePending PassRuleResult = iota // Still waiting for more actions PassRulePassed // Node passed PassRuleRejected // Node rejected )
type PassRuleStrategy ¶
type PassRuleStrategy interface {
// Rule returns the pass rule this strategy handles.
Rule() PassRule
// Evaluate determines the pass/reject/pending outcome based on task approval counts.
Evaluate(ctx PassRuleContext) PassRuleResult
}
PassRuleStrategy evaluates whether a node passes based on task results.
type Permission ¶
type Permission string
Permission represents the permission level.
const ( PermissionVisible Permission = "visible" PermissionEditable Permission = "editable" PermissionHidden Permission = "hidden" PermissionRequired Permission = "required" )
func (Permission) IsValid ¶ added in v0.38.0
func (p Permission) IsValid() bool
IsValid reports whether the permission is one of the defined values.
type PrincipalDepartmentResolver ¶ added in v0.20.1
type PrincipalDepartmentResolver interface {
// Resolve extracts the department ID and name from the given security principal.
Resolve(ctx context.Context, principal *security.Principal) (departmentID, departmentName *string, err error)
}
PrincipalDepartmentResolver resolves department info from a security principal. Implemented by host apps since Principal.Details is business-specific.
type PrincipalTenantResolver ¶ added in v0.24.0
type PrincipalTenantResolver interface {
// Resolve returns the tenant identifier for the given principal.
Resolve(ctx context.Context, principal *security.Principal) (string, error)
}
PrincipalTenantResolver extracts the caller's tenant ID from a security principal. Implemented by host applications because Principal.Details is schema-less; the framework cannot know where the host stores tenant affiliation.
Returning an empty string is only meaningful for super-admin or system-internal callers, which bypass tenant scoping. For an ordinary authenticated principal an empty tenant fails closed: CallerContext.Authorize returns ErrCrossTenantAccess and TenantScopeFilter / ResolveWriteTenant likewise reject it, rather than waving the request through.
type ResolvedAssignee ¶
ResolvedAssignee represents a resolved assignee with optional delegation info: User is who receives the task; Delegator is set when the task arrived via delegation and names the original assignee.
type RoleMembershipChecker ¶ added in v0.29.0
type RoleMembershipChecker interface {
// UserHasRole reports whether the user currently holds the role.
UserHasRole(ctx context.Context, userID, roleID string) (bool, error)
}
RoleMembershipChecker is an optional capability of AssigneeService. Hosts that can answer "does this user hold this role" directly should implement it: membership checks (e.g. role-based initiation permission) then skip the GetRoleUsers full-listing fallback, which scales poorly for large roles. Detected via type assertion, so existing implementations keep working unchanged.
type RollbackDataStrategy ¶
type RollbackDataStrategy string
RollbackDataStrategy represents the strategy for handling form data during rollback.
const ( RollbackDataClear RollbackDataStrategy = "clear" // Clear form data RollbackDataKeep RollbackDataStrategy = "keep" // Restore the form snapshot captured when the target node was first entered )
func (RollbackDataStrategy) IsValid ¶ added in v0.29.0
func (s RollbackDataStrategy) IsValid() bool
IsValid reports whether the rollback data strategy is one of the defined values.
type RollbackType ¶
type RollbackType string
RollbackType represents the type of rollback allowed.
const ( RollbackNone RollbackType = "none" RollbackPrevious RollbackType = "previous" // To previous node RollbackStart RollbackType = "start" // To start node (applicant) RollbackAny RollbackType = "any" // To any node RollbackSpecified RollbackType = "specified" // To specified nodes )
func (RollbackType) IsValid ¶ added in v0.29.0
func (t RollbackType) IsValid() bool
IsValid reports whether the rollback type is one of the defined values.
type SameApplicantAction ¶
type SameApplicantAction string
SameApplicantAction represents the action when the assignee is the same as the applicant.
const ( SameApplicantAutoPass SameApplicantAction = "auto_pass" SameApplicantSelfApprove SameApplicantAction = "self_approve" // Default SameApplicantTransferSuperior SameApplicantAction = "transfer_superior" // Transfer to superior )
func (SameApplicantAction) IsValid ¶ added in v0.29.0
func (a SameApplicantAction) IsValid() bool
IsValid reports whether the same-applicant action is one of the defined values.
type StartNodeData ¶
type StartNodeData struct {
BaseNodeData
}
StartNodeData contains data specific to start nodes.
func (*StartNodeData) ApplyTo ¶
func (d *StartNodeData) ApplyTo(node *FlowNode)
ApplyTo applies start node data to a FlowNode.
type StorageMode ¶
type StorageMode string
StorageMode represents the storage mode of form data at the FlowVersion level. It determines the physical storage location and format of form data, and is fixed when a version is published. This is different from BindingMode (Flow-level), which controls how the workflow integrates with business systems.
Usage scenarios:
- JSON mode: Flexible schema, suitable for frequently changing form fields, limited query capabilities
- Table mode: Structured storage, suitable for complex queries and data analysis, requires predefined schema
const ( // StorageJSON stores form data in the apv_instance.form_data JSONB column. StorageJSON StorageMode = "json" // StorageTable stores form data in a dedicated physical table generated per // published version (one table per version). The apv_instance.form_data // JSONB column is still populated so existing read paths keep working; the // physical table is the structured, queryable projection. StorageTable StorageMode = "table" )
func (StorageMode) IsValid ¶ added in v0.32.0
func (m StorageMode) IsValid() bool
IsValid reports whether the storage mode is one of the defined values.
type Task ¶
type Task struct {
orm.BaseModel `bun:"table:apv_task,alias:at"`
orm.FullAuditedModel
TenantID string `json:"tenantId" bun:"tenant_id"`
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID string `json:"nodeId" bun:"node_id"`
VisitID string `json:"visitId" bun:"visit_id"`
AssigneeID string `json:"assigneeId" bun:"assignee_id"`
AssigneeName string `json:"assigneeName" bun:"assignee_name"`
AssigneeDepartmentID *string `json:"assigneeDepartmentId" bun:"assignee_department_id,nullzero"`
AssigneeDepartmentName *string `json:"assigneeDepartmentName" bun:"assignee_department_name,nullzero"`
DelegatorID *string `json:"delegatorId" bun:"delegator_id,nullzero"`
DelegatorName *string `json:"delegatorName" bun:"delegator_name,nullzero"`
DelegatorDepartmentID *string `json:"delegatorDepartmentId" bun:"delegator_department_id,nullzero"`
DelegatorDepartmentName *string `json:"delegatorDepartmentName" bun:"delegator_department_name,nullzero"`
SortOrder int `json:"sortOrder" bun:"sort_order"`
Status TaskStatus `json:"status" bun:"status"`
ReadAt *timex.DateTime `json:"readAt" bun:"read_at,nullzero"`
ParentTaskID *string `json:"parentTaskId" bun:"parent_task_id,nullzero"`
AddAssigneeType *AddAssigneeType `json:"addAssigneeType" bun:"add_assignee_type,nullzero"`
Deadline *timex.DateTime `json:"deadline" bun:"deadline,nullzero"`
IsTimeout bool `json:"isTimeout" bun:"is_timeout"`
IsPreWarningSent bool `json:"isPreWarningSent" bun:"is_pre_warning_sent"`
FinishedAt *timex.DateTime `json:"finishedAt" bun:"finished_at,nullzero"`
}
Task represents an approval task.
type TaskApprovedEvent ¶
type TaskApprovedEvent struct {
TaskEventBase
Operator UserInfo `json:"operator"`
Opinion *string `json:"opinion,omitempty"`
}
TaskApprovedEvent fired when a task is approved.
func NewTaskApprovedEvent ¶
func (*TaskApprovedEvent) EventType ¶ added in v0.24.0
func (*TaskApprovedEvent) EventType() string
type TaskCanceledEvent ¶ added in v0.29.0
type TaskCanceledEvent struct {
TaskEventBase
Assignee UserInfo `json:"assignee"`
Reason string `json:"reason"`
}
TaskCanceledEvent fired when the engine cancels a task that no longer needs a decision — its node completed through other votes, or the whole instance was withdrawn, rolled back, or terminated. Subscribers use it to retract pending to-do entries for the canceled assignee. Reason carries the triggering operation.
func NewTaskCanceledEvent ¶ added in v0.29.0
func NewTaskCanceledEvent(instance *Instance, task *Task, node *FlowNode, reason string) *TaskCanceledEvent
func (*TaskCanceledEvent) EventType ¶ added in v0.29.0
func (*TaskCanceledEvent) EventType() string
type TaskCreatedEvent ¶
type TaskCreatedEvent struct {
TaskEventBase
Assignee UserInfo `json:"assignee"`
Deadline *timex.DateTime `json:"deadline,omitempty"`
}
TaskCreatedEvent fires the moment a task row is inserted, not the moment it becomes actionable. Under sequential approval, tasks after the first start with Status=Waiting and a nil Deadline; subscribers should treat a nil Deadline as the cue that the task is queued behind a predecessor and must not yet surface a "new pending task" notification. When the predecessor finishes and the task transitions to Pending, no new event is published — subscribers reading the live task table see the change. If this contract proves insufficient, the right extension is to add a dedicated TaskActivatedEvent rather than overloading TaskCreatedEvent.
func NewTaskCreatedEvent ¶
func NewTaskCreatedEvent(instance *Instance, task *Task, node *FlowNode) *TaskCreatedEvent
func (*TaskCreatedEvent) EventType ¶ added in v0.24.0
func (*TaskCreatedEvent) EventType() string
type TaskDeadlineWarningEvent ¶
type TaskDeadlineWarningEvent struct {
TaskEventBase
Assignee UserInfo `json:"assignee"`
Deadline timex.DateTime `json:"deadline"`
HoursLeft int `json:"hoursLeft"`
}
TaskDeadlineWarningEvent fired ahead of a task's deadline so subscribers can nudge the assignee before the timeout action kicks in.
func NewTaskDeadlineWarningEvent ¶
func NewTaskDeadlineWarningEvent(instance *Instance, task *Task, node *FlowNode, hoursLeft int) *TaskDeadlineWarningEvent
func (*TaskDeadlineWarningEvent) EventType ¶ added in v0.24.0
func (*TaskDeadlineWarningEvent) EventType() string
type TaskEventBase ¶ added in v0.35.0
type TaskEventBase struct {
InstanceEventBase
TaskID string `json:"taskId"`
NodeID string `json:"nodeId"`
NodeName string `json:"nodeName"`
}
TaskEventBase extends the instance envelope with the task coordinates a to-do style subscriber needs: which task, on which node, under which name.
func NewTaskEventBase ¶ added in v0.35.0
func NewTaskEventBase(instance *Instance, task *Task, node *FlowNode) TaskEventBase
NewTaskEventBase snapshots the instance, task, and node coordinates into the shared task event envelope.
type TaskHandledEvent ¶
type TaskHandledEvent struct {
TaskEventBase
Operator UserInfo `json:"operator"`
Opinion *string `json:"opinion,omitempty"`
}
TaskHandledEvent fired when a handle-type task is completed.
func NewTaskHandledEvent ¶
func (*TaskHandledEvent) EventType ¶ added in v0.24.0
func (*TaskHandledEvent) EventType() string
type TaskNodeData ¶
type TaskNodeData struct {
Assignees []AssigneeDefinition `json:"assignees,omitempty"`
ExecutionType ExecutionType `json:"executionType,omitempty"`
EmptyAssigneeAction EmptyAssigneeAction `json:"emptyAssigneeAction,omitempty"`
FallbackUserIDs []string `json:"fallbackUserIds,omitempty"`
AdminUserIDs []string `json:"adminUserIds,omitempty"`
IsTransferAllowed *bool `json:"isTransferAllowed,omitempty"`
IsOpinionRequired bool `json:"isOpinionRequired,omitempty"`
TimeoutHours int `json:"timeoutHours,omitempty"`
TimeoutAction TimeoutAction `json:"timeoutAction,omitempty"`
TimeoutNotifyBeforeHours int `json:"timeoutNotifyBeforeHours,omitempty"`
UrgeCooldownMinutes int `json:"urgeCooldownMinutes,omitempty"`
CCs []CCDefinition `json:"ccs,omitempty"`
FieldPermissions map[string]Permission `json:"fieldPermissions,omitempty"`
}
TaskNodeData contains fields shared by approval and handle nodes.
IsTransferAllowed is a pointer because its default is true: an omitted field must resolve to "allowed", which a plain bool zero value cannot represent.
func (*TaskNodeData) GetAssignees ¶
func (d *TaskNodeData) GetAssignees() []AssigneeDefinition
GetAssignees returns the assignee definitions from TaskNodeData.
func (*TaskNodeData) GetCCs ¶
func (d *TaskNodeData) GetCCs() []CCDefinition
GetCCs returns the CC definitions from TaskNodeData.
type TaskReassignedEvent ¶
type TaskReassignedEvent struct {
TaskEventBase
From UserInfo `json:"from"`
To UserInfo `json:"to"`
Reason *string `json:"reason,omitempty"`
}
TaskReassignedEvent fired when an admin reassigns a task to a different user.
func NewTaskReassignedEvent ¶
func (*TaskReassignedEvent) EventType ¶ added in v0.24.0
func (*TaskReassignedEvent) EventType() string
type TaskRejectedEvent ¶
type TaskRejectedEvent struct {
TaskEventBase
Operator UserInfo `json:"operator"`
Opinion *string `json:"opinion,omitempty"`
}
TaskRejectedEvent fired when a task is rejected.
func NewTaskRejectedEvent ¶
func (*TaskRejectedEvent) EventType ¶ added in v0.24.0
func (*TaskRejectedEvent) EventType() string
type TaskStatus ¶
type TaskStatus string
TaskStatus represents the status of an approval task.
const ( TaskWaiting TaskStatus = "waiting" TaskPending TaskStatus = "pending" TaskApproved TaskStatus = "approved" TaskRejected TaskStatus = "rejected" TaskHandled TaskStatus = "handled" TaskTransferred TaskStatus = "transferred" TaskRolledBack TaskStatus = "rolled_back" TaskCanceled TaskStatus = "canceled" TaskRemoved TaskStatus = "removed" TaskSkipped TaskStatus = "skipped" )
func (TaskStatus) IsFinal ¶
func (s TaskStatus) IsFinal() bool
func (TaskStatus) String ¶
func (s TaskStatus) String() string
type TaskTimedOutEvent ¶ added in v0.24.0
type TaskTimedOutEvent struct {
TaskEventBase
Assignee UserInfo `json:"assignee"`
Deadline timex.DateTime `json:"deadline"`
}
TaskTimedOutEvent fired when a task times out.
func NewTaskTimedOutEvent ¶ added in v0.24.0
func NewTaskTimedOutEvent(instance *Instance, task *Task, node *FlowNode) *TaskTimedOutEvent
func (*TaskTimedOutEvent) EventType ¶ added in v0.24.0
func (*TaskTimedOutEvent) EventType() string
type TaskTransferredEvent ¶
type TaskTransferredEvent struct {
TaskEventBase
From UserInfo `json:"from"`
To UserInfo `json:"to"`
Reason *string `json:"reason,omitempty"`
}
TaskTransferredEvent fired when a task is transferred by its assignee.
func NewTaskTransferredEvent ¶
func (*TaskTransferredEvent) EventType ¶ added in v0.24.0
func (*TaskTransferredEvent) EventType() string
type TaskUrgedEvent ¶
type TaskUrgedEvent struct {
TaskEventBase
Urger UserInfo `json:"urger"`
Target UserInfo `json:"target"`
Message *string `json:"message,omitempty"`
}
TaskUrgedEvent fired when a participant urges the pending assignee.
func NewTaskUrgedEvent ¶
func (*TaskUrgedEvent) EventType ¶ added in v0.24.0
func (*TaskUrgedEvent) EventType() string
type TimelineEntry ¶ added in v0.35.0
type TimelineEntry struct {
Kind TimelineEntryKind `json:"kind"`
NodeID *string `json:"nodeId,omitempty"`
Name string `json:"name,omitempty"`
Status NodeVisitStatus `json:"status,omitempty"`
ExecutionType string `json:"executionType,omitempty"`
ApprovalMethod string `json:"approvalMethod,omitempty"`
PassRule string `json:"passRule,omitempty"`
PassRatio *decimal.Decimal `json:"passRatio,omitempty"`
Participants []NodeParticipant `json:"participants,omitempty"`
CCRecipients []CCRecipient `json:"ccRecipients,omitempty"`
Activities []Activity `json:"activities,omitempty"`
StartedAt timex.DateTime `json:"startedAt"`
FinishedAt *timex.DateTime `json:"finishedAt,omitempty"`
}
TimelineEntry is one step of the instance timeline — the chronological, node-by-node account of the path an instance actually took, ready to render as a transit-record list without client-side reshaping. Because condition branches are exclusive, the traversed path is always a single line; a node re-entered after a rollback produces a second entry. Entries end at the node currently in progress — unreached nodes are not predicted.
Node entries carry the node's display config and the people involved: Participants for approval/handle entries (one per task, fused with the log that finished it), CCRecipients for delivered carbon copies (both cc nodes and timing-based CC configured on approval/handle nodes), and Activities for side actions that happened at the node (transfer, rollback, add/remove assignee, manual CC, urge, system execution — and submit/resubmit on start entries). Milestone entries (withdraw / terminate) hold a single activity describing who closed the instance and why.
type TimelineEntryKind ¶ added in v0.35.0
type TimelineEntryKind string
TimelineEntryKind classifies one entry of an instance timeline. Node-visit entries reuse the node-kind vocabulary (start / approval / handle / cc); instance-level milestones (withdraw / terminate) carry their action name. Structural kinds (condition / end) never appear — they route or close the flow without anything to narrate.
const ( TimelineEntryStart TimelineEntryKind = "start" TimelineEntryApproval TimelineEntryKind = "approval" TimelineEntryHandle TimelineEntryKind = "handle" TimelineEntryCC TimelineEntryKind = "cc" TimelineEntryWithdraw TimelineEntryKind = "withdraw" TimelineEntryTerminate TimelineEntryKind = "terminate" )
type TimeoutAction ¶
type TimeoutAction string
TimeoutAction represents the action to take when a task times out.
const ( TimeoutActionNone TimeoutAction = "none" // Mark timeout only, no auto action TimeoutActionAutoPass TimeoutAction = "auto_pass" // Automatically approve the task TimeoutActionAutoReject TimeoutAction = "auto_reject" // Automatically reject the task TimeoutActionNotify TimeoutAction = "notify" // Send notification only TimeoutActionTransferAdmin TimeoutAction = "transfer_admin" // Transfer to node admin )
func (TimeoutAction) IsValid ¶ added in v0.29.0
func (a TimeoutAction) IsValid() bool
IsValid reports whether the timeout action is one of the defined values.
type UrgeRecord ¶
type UrgeRecord struct {
orm.BaseModel `bun:"table:apv_urge_record,alias:aur"`
orm.Model
orm.CreationTrackedModel
InstanceID string `json:"instanceId" bun:"instance_id"`
NodeID string `json:"nodeId" bun:"node_id"`
TaskID *string `json:"taskId" bun:"task_id,nullzero"`
UrgerID string `json:"urgerId" bun:"urger_id"`
UrgerName string `json:"urgerName" bun:"urger_name"`
UrgerDepartmentID *string `json:"urgerDepartmentId" bun:"urger_department_id,nullzero"`
UrgerDepartmentName *string `json:"urgerDepartmentName" bun:"urger_department_name,nullzero"`
TargetUserID string `json:"targetUserId" bun:"target_user_id"`
TargetUserName string `json:"targetUserName" bun:"target_user_name"`
TargetUserDepartmentID *string `json:"targetUserDepartmentId" bun:"target_user_department_id,nullzero"`
TargetUserDepartmentName *string `json:"targetUserDepartmentName" bun:"target_user_department_name,nullzero"`
Message string `json:"message" bun:"message"`
}
UrgeRecord represents an urge/reminder record.
func (*UrgeRecord) Target ¶ added in v0.35.0
func (r *UrgeRecord) Target() UserInfo
Target returns the urged assignee as a person snapshot.
func (*UrgeRecord) Urger ¶ added in v0.35.0
func (r *UrgeRecord) Urger() UserInfo
Urger returns the urging user as a person snapshot.
type UserInfo ¶
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
DepartmentID *string `json:"departmentId,omitempty"`
DepartmentName *string `json:"departmentName,omitempty"`
}
UserInfo is the approval module's uniform person reference — identity plus optional department — used everywhere a user appears: resolver lookups, command operators, persisted snapshots (task assignees and delegators, CC recipients, urge parties, action-log person lists), and detail projections, so a client renders every person with one component and no second lookup. The role a value plays is carried by the field holding it (Operator, Applicant, User, Delegator, TransferTo, …), never by a separate type. Department fields hold whatever the writing side captured at the time and are omitted when unknown; hosts opt in by filling them in UserInfoResolver.ResolveUsers.
func (UserInfo) NewActionLog ¶ added in v0.35.0
func (u UserInfo) NewActionLog(instanceID string, action ActionType) *ActionLog
NewActionLog creates an ActionLog with the operator fields pre-filled from the acting user.
type UserInfoResolver ¶
type UserInfoResolver interface {
// ResolveUsers returns user info for the given IDs.
// Missing IDs should be returned with empty Name (not omitted).
// Department fields are optional; fill them to enrich the person
// snapshots the approval module records.
ResolveUsers(ctx context.Context, userIDs []string) (map[string]UserInfo, error)
}
UserInfoResolver resolves user display info by IDs (implemented by host app).
type ValidationRule ¶
type ValidationRule struct {
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Min *float64 `json:"min,omitempty"`
Max *float64 `json:"max,omitempty"`
Pattern string `json:"pattern,omitempty"`
Message string `json:"message,omitempty"`
}
ValidationRule contains validation constraints for a form field.
type VersionStatus ¶
type VersionStatus string
VersionStatus represents the status of a flow version.
const ( VersionDraft VersionStatus = "draft" VersionPublished VersionStatus = "published" VersionArchived VersionStatus = "archived" )
Source Files
¶
- assignee.go
- binding.go
- command_bridge.go
- condition.go
- enums.go
- events.go
- events_base.go
- events_cc.go
- events_flow.go
- events_instance.go
- events_node.go
- events_task.go
- events_timeout.go
- flow_definition.go
- flow_graph_view.go
- form_data.go
- form_field.go
- form_schema.go
- lifecycle_hook.go
- models.go
- node_data.go
- node_view.go
- subscribe.go
- tenancy.go
- timeline_view.go