approval

package
v0.38.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

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

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

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

View Source
const DefaultTenantID = "default"

DefaultTenantID is the tenant identifier used when a caller does not carry an explicit tenant — the conventional single-tenant deployment value.

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

View Source
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")
)
View Source
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.

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

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

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

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

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

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

func IsSuperAdmin(p *security.Principal) bool

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

func ValidateBusinessIdentifier(id string) error

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) Operator added in v0.35.0

func (l *ActionLog) Operator() UserInfo

Operator returns the operator as a person snapshot.

func (*ActionLog) TransferTo added in v0.35.0

func (l *ActionLog) TransferTo() *UserInfo

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.

func (BaseNodeData) GetName

func (d BaseNodeData) GetName() string

GetName returns the node name.

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

type BusinessRecordKey map[string]any

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 CCKind

type CCKind string

CCKind represents the kind of CC recipient.

const (
	CCUser       CCKind = "user"
	CCRole       CCKind = "role"
	CCDepartment CCKind = "department"
	CCFormField  CCKind = "form_field"
)

func (CCKind) IsValid added in v0.29.0

func (k CCKind) IsValid() bool

IsValid reports whether the CC kind is one of the defined kinds.

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.

func (*CCNodeData) Kind

func (*CCNodeData) Kind() NodeKind

Kind returns the node kind.

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 CCTiming

type CCTiming string

CCTiming represents the timing of CC notification.

const (
	CCTimingAlways    CCTiming = "always"     // Always: send CC regardless of result
	CCTimingOnApprove CCTiming = "on_approve" // OnApprove: send CC only when approved
	CCTimingOnReject  CCTiming = "on_reject"  // OnReject: send CC only when rejected
)

func (CCTiming) IsValid added in v0.29.0

func (t CCTiming) IsValid() bool

IsValid reports whether the CC timing is one of the defined values.

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.

func (*EndNodeData) Kind

func (*EndNodeData) Kind() NodeKind

Kind returns the node kind.

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"
)

func (FieldKind) IsValid added in v0.29.0

func (k FieldKind) IsValid() bool

IsValid reports whether the field kind is one of the defined values.

type FieldOption

type FieldOption struct {
	Label string `json:"label"`
	Value any    `json:"value"`
}

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

type FormData map[string]any

FormData wraps a map to provide helper methods for form data operations.

func NewFormData

func NewFormData(data map[string]any) FormData

func (FormData) Clone

func (f FormData) Clone() (FormData, error)

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.

func (FormData) Get

func (f FormData) Get(key string) any

func (FormData) Set

func (f FormData) Set(key string, val any)

func (FormData) ToMap

func (f FormData) ToMap() map[string]any

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).

func (*HandleNodeData) Kind

func (*HandleNodeData) Kind() NodeKind

Kind returns the node kind.

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.

func (*Instance) Applicant added in v0.35.0

func (i *Instance) Applicant() UserInfo

Applicant returns the applicant as a person snapshot.

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

type InstanceEvent interface {
	event.Event
	// contains filtered or unexported methods
}

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

type InstanceFilter struct {
	FlowCodes []string
	TenantIDs []string
}

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).

const (
	PassAll   PassRule = "all"   // All assignees must approve; any rejection fails the node
	PassAny   PassRule = "any"   // At least one assignee must approve
	PassRatio PassRule = "ratio" // A certain percentage of assignees must approve
)

func (PassRule) IsValid added in v0.29.0

func (r PassRule) IsValid() bool

IsValid reports whether the pass rule is one of the defined values.

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 Position

type Position struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}

Position represents the visual position of a node on the canvas.

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

type ResolvedAssignee struct {
	User      UserInfo
	Delegator *UserInfo
}

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.

func (*StartNodeData) Kind

func (*StartNodeData) Kind() NodeKind

Kind returns the node kind.

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.

func (*Task) Assignee added in v0.35.0

func (t *Task) Assignee() UserInfo

Assignee returns the task assignee as a person snapshot.

func (*Task) Delegator added in v0.35.0

func (t *Task) Delegator() *UserInfo

Delegator returns the delegator as a person snapshot, or nil when the task did not arrive via delegation.

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 NewTaskApprovedEvent(instance *Instance, task *Task, node *FlowNode, operator UserInfo, opinion string) *TaskApprovedEvent

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 NewTaskHandledEvent(instance *Instance, task *Task, node *FlowNode, operator UserInfo, opinion string) *TaskHandledEvent

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 NewTaskReassignedEvent(instance *Instance, task *Task, node *FlowNode, from, to UserInfo, reason string) *TaskReassignedEvent

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 NewTaskRejectedEvent(instance *Instance, task *Task, node *FlowNode, operator UserInfo, opinion string) *TaskRejectedEvent

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 NewTaskTransferredEvent(instance *Instance, task *Task, node *FlowNode, from, to UserInfo, reason string) *TaskTransferredEvent

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 NewTaskUrgedEvent(instance *Instance, task *Task, node *FlowNode, urger UserInfo, message string) *TaskUrgedEvent

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"
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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