490 lines
20 KiB
Go
490 lines
20 KiB
Go
// Package integrationlog 提供外部交互尝试的可靠持久化能力。
|
|
package integrationlog
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
|
|
)
|
|
|
|
// Attempt 描述一次外部调用前必须持久化的稳定事实。
|
|
type Attempt struct {
|
|
IntegrationID string
|
|
Provider string
|
|
Direction string
|
|
Operation string
|
|
ExternalID *string
|
|
ResourceType string
|
|
ResourceID *string
|
|
ResourceKey *string
|
|
TriggerSource *string
|
|
TriggerScene *string
|
|
TriggerSeries *string
|
|
ScheduledAt *time.Time
|
|
StartedAt *time.Time
|
|
Attempt int
|
|
RequestSummary any
|
|
Metadata any
|
|
RequestID *string
|
|
CorrelationID *string
|
|
AuditEventID *uint
|
|
InitialResult string
|
|
StateChanged bool
|
|
RecoveryStrategy *string
|
|
}
|
|
|
|
// Completion 描述外部尝试从待处理状态进入终态的结果。
|
|
type Completion struct {
|
|
Result string
|
|
HTTPStatus int
|
|
ProviderCode string
|
|
ProviderMessage string
|
|
SafeProviderMessage string
|
|
ResponseSummary any
|
|
DurationMS int64
|
|
StateChanged bool
|
|
ResourceID *string
|
|
ResourceKey *string
|
|
RecoveryStrategy string
|
|
}
|
|
|
|
// InboundAttempt 描述业务处理前必须保存的入站回调安全事实。
|
|
type InboundAttempt struct {
|
|
IntegrationID string
|
|
IdempotencyKey string
|
|
Provider string
|
|
Operation string
|
|
ExternalID string
|
|
ResourceType string
|
|
ResourceID *string
|
|
ResourceKey *string
|
|
RawPayload []byte
|
|
ContentType string
|
|
RequestID *string
|
|
CorrelationID *string
|
|
AuditEventID *uint
|
|
}
|
|
|
|
// Repository 负责创建稳定尝试及受控地进入终态。
|
|
type Repository struct {
|
|
db *gorm.DB
|
|
now func() time.Time
|
|
audit *audit.Writer
|
|
}
|
|
|
|
// NewRepository 创建 Integration Log Repository。
|
|
func NewRepository(db *gorm.DB) *Repository {
|
|
return &Repository{db: db, now: time.Now, audit: audit.NewWriter(audit.NewRegistry(), nil)}
|
|
}
|
|
|
|
// Start 在实际调用外部系统前持久化尝试事实。
|
|
func (r *Repository) Start(ctx context.Context, input Attempt) (*model.IntegrationLog, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
|
|
}
|
|
if err := validateAttempt(input); err != nil {
|
|
return nil, err
|
|
}
|
|
requestSummary, err := marshalSummary(input.RequestSummary)
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeInvalidParam, err, "Integration Log 请求摘要无效")
|
|
}
|
|
metadata, err := marshalSummary(input.Metadata)
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeInvalidParam, err, "Integration Log 元数据无效")
|
|
}
|
|
if input.IntegrationID == "" {
|
|
input.IntegrationID = uuid.NewString()
|
|
}
|
|
if input.AuditEventID == nil && input.TriggerSeries != nil {
|
|
input.AuditEventID = r.auditEventIDForSeries(ctx, *input.TriggerSeries)
|
|
}
|
|
if input.AuditEventID == nil {
|
|
input.AuditEventID = r.recordAuditEvent(ctx, constants.AuditActionIntegrationAttemptStarted, input.IntegrationID, input.Provider, input.Direction, input.Operation, input.ResourceType, input.ResourceID, input.ResourceKey, input.CorrelationID)
|
|
}
|
|
autoAttempt := input.Attempt <= 0 && input.TriggerSeries != nil
|
|
if input.Attempt <= 0 {
|
|
input.Attempt = 1
|
|
}
|
|
if input.StartedAt == nil {
|
|
startedAt := r.now().UTC()
|
|
input.StartedAt = &startedAt
|
|
}
|
|
result := input.InitialResult
|
|
if result == "" {
|
|
result = constants.IntegrationResultPending
|
|
}
|
|
resourceType := optionalString(input.ResourceType)
|
|
log := &model.IntegrationLog{
|
|
IntegrationID: input.IntegrationID, Provider: input.Provider, Direction: input.Direction,
|
|
Operation: input.Operation, ExternalID: sanitizedOptionalText(input.ExternalID), ResourceType: resourceType,
|
|
ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), TriggerSource: input.TriggerSource,
|
|
TriggerScene: sanitizedOptionalText(input.TriggerScene), TriggerSeries: input.TriggerSeries, ScheduledAt: input.ScheduledAt,
|
|
StartedAt: input.StartedAt, Attempt: input.Attempt, Result: result, StateChanged: input.StateChanged,
|
|
RequestSummary: requestSummary, Metadata: metadata, RequestID: input.RequestID,
|
|
CorrelationID: input.CorrelationID, AuditEventID: input.AuditEventID,
|
|
RecoveryStrategy: sanitizedOptionalText(input.RecoveryStrategy),
|
|
}
|
|
createAttempt := func(tx *gorm.DB) error {
|
|
if !autoAttempt {
|
|
return tx.Create(log).Error
|
|
}
|
|
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", *input.TriggerSeries).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&model.IntegrationLog{}).Select("COALESCE(MAX(attempt), 0) + 1").
|
|
Where("trigger_series = ?", *input.TriggerSeries).Scan(&log.Attempt).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Create(log).Error
|
|
}
|
|
var createErr error
|
|
if autoAttempt {
|
|
createErr = r.db.WithContext(ctx).Transaction(createAttempt)
|
|
} else {
|
|
createErr = createAttempt(r.db.WithContext(ctx))
|
|
}
|
|
if createErr != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, createErr, "写入 Integration Log 失败")
|
|
}
|
|
return log, nil
|
|
}
|
|
|
|
// Complete 仅允许把待处理尝试条件更新为一个公开终态。
|
|
func (r *Repository) Complete(ctx context.Context, integrationID string, completion Completion) (*model.IntegrationLog, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
|
|
}
|
|
if !validRequiredString(integrationID, constants.IntegrationIDMaxLength) ||
|
|
!validOptionalString(completion.ResourceID, constants.IntegrationResourceIDMaxLength) ||
|
|
!validOptionalString(completion.ResourceKey, constants.IntegrationResourceKeyMaxLength) ||
|
|
!isTerminalResult(completion.Result) {
|
|
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 终态参数无效")
|
|
}
|
|
if completion.Result == constants.IntegrationResultUnknown && strings.TrimSpace(completion.RecoveryStrategy) == "" {
|
|
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "结果未知必须记录明确恢复策略")
|
|
}
|
|
safeProviderMessage := sanitizer.SanitizeText(strings.TrimSpace(completion.SafeProviderMessage))
|
|
if safeProviderMessage != "" && utf8.RuneCountInString(constants.IntegrationSafeMessagePrefix+safeProviderMessage) > constants.IntegrationProviderMessageMaxLength {
|
|
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 安全结果摘要过长")
|
|
}
|
|
responseSummary, err := marshalSummary(completion.ResponseSummary)
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeInvalidParam, err, "Integration Log 响应摘要无效")
|
|
}
|
|
updates := map[string]any{
|
|
"result": completion.Result, "duration_ms": completion.DurationMS,
|
|
"state_changed": completion.StateChanged, "response_summary": responseSummary,
|
|
"updated_at": r.now().UTC(),
|
|
}
|
|
if completion.HTTPStatus != 0 {
|
|
updates["http_status"] = completion.HTTPStatus
|
|
}
|
|
if completion.ProviderCode != "" {
|
|
updates["provider_code"] = completion.ProviderCode
|
|
}
|
|
if safeProviderMessage != "" {
|
|
updates["provider_message"] = constants.IntegrationSafeMessagePrefix + safeProviderMessage
|
|
} else if completion.ProviderMessage != "" {
|
|
updates["provider_message"] = sanitizer.TextSummary(completion.ProviderMessage)
|
|
}
|
|
if completion.ResourceID != nil {
|
|
updates["resource_id"] = completion.ResourceID
|
|
}
|
|
if completion.ResourceKey != nil {
|
|
updates["resource_key"] = sanitizedOptionalText(completion.ResourceKey)
|
|
}
|
|
if completion.RecoveryStrategy != "" {
|
|
updates["recovery_strategy"] = sanitizer.SanitizeText(completion.RecoveryStrategy)
|
|
}
|
|
result := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
|
|
Where("integration_id = ? AND result = ?", integrationID, constants.IntegrationResultPending).
|
|
Updates(updates)
|
|
if result.Error != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, result.Error, "终结 Integration Log 失败")
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return nil, pkgerrors.New(pkgerrors.CodeConflict, "Integration Log 已进入终态或不存在")
|
|
}
|
|
var saved model.IntegrationLog
|
|
if err := r.db.WithContext(ctx).Where("integration_id = ?", integrationID).First(&saved).Error; err != nil {
|
|
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "读取 Integration Log 终态失败")
|
|
}
|
|
return &saved, nil
|
|
}
|
|
|
|
// RecordInbound 在业务处理前幂等保存入站回调的安全摘要。
|
|
func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (*model.IntegrationLog, bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, false, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
|
|
}
|
|
if input.Provider == "" || input.Operation == "" || input.IdempotencyKey == "" ||
|
|
!validGeneratedString(input.IntegrationID, constants.IntegrationIDMaxLength) ||
|
|
!validOptionalString(input.ResourceID, constants.IntegrationResourceIDMaxLength) ||
|
|
!validOptionalString(input.ResourceKey, constants.IntegrationResourceKeyMaxLength) ||
|
|
!validOptionalString(input.CorrelationID, constants.IntegrationCorrelationIDMaxLength) {
|
|
return nil, false, pkgerrors.New(pkgerrors.CodeInvalidParam, "入站 Integration Log 参数无效")
|
|
}
|
|
if input.IntegrationID == "" {
|
|
input.IntegrationID = uuid.NewString()
|
|
}
|
|
if input.AuditEventID == nil {
|
|
input.AuditEventID = r.recordAuditEvent(ctx, constants.AuditActionIntegrationInboundReceived, input.IntegrationID, input.Provider, constants.IntegrationDirectionInbound, input.Operation, input.ResourceType, input.ResourceID, input.ResourceKey, input.CorrelationID)
|
|
}
|
|
triggerSeries := input.IntegrationID
|
|
hash := sha256.Sum256(input.RawPayload)
|
|
summary, err := marshalSummary(map[string]any{
|
|
"content_type": input.ContentType,
|
|
"payload_bytes": len(input.RawPayload),
|
|
"content_hash": hex.EncodeToString(hash[:]),
|
|
})
|
|
if err != nil {
|
|
return nil, false, pkgerrors.Wrap(pkgerrors.CodeInvalidParam, err, "入站 Integration Log 摘要无效")
|
|
}
|
|
now := r.now().UTC()
|
|
log := &model.IntegrationLog{
|
|
IntegrationID: input.IntegrationID, IdempotencyKey: &input.IdempotencyKey,
|
|
Provider: input.Provider, Direction: constants.IntegrationDirectionInbound, Operation: input.Operation,
|
|
ExternalID: sanitizedOptionalText(optionalString(input.ExternalID)), ResourceType: optionalString(input.ResourceType),
|
|
ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), StartedAt: &now, Attempt: 1,
|
|
TriggerSeries: &triggerSeries,
|
|
Result: constants.IntegrationResultPending, RequestSummary: summary,
|
|
ContentHash: hex.EncodeToString(hash[:]), RequestID: input.RequestID, CorrelationID: input.CorrelationID,
|
|
AuditEventID: input.AuditEventID,
|
|
}
|
|
result := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "provider"}, {Name: "operation"}, {Name: "idempotency_key"}},
|
|
DoNothing: true,
|
|
}).Create(log)
|
|
if result.Error != nil {
|
|
return nil, false, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, result.Error, "写入入站 Integration Log 失败")
|
|
}
|
|
if result.RowsAffected == 1 {
|
|
return log, true, nil
|
|
}
|
|
var existing model.IntegrationLog
|
|
if err := r.db.WithContext(ctx).Where(
|
|
"provider = ? AND operation = ? AND idempotency_key = ?", input.Provider, input.Operation, input.IdempotencyKey,
|
|
).First(&existing).Error; err != nil {
|
|
return nil, false, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "读取重复入站 Integration Log 失败")
|
|
}
|
|
if existing.ContentHash != hex.EncodeToString(hash[:]) {
|
|
return nil, false, pkgerrors.New(pkgerrors.CodeConflict, "入站幂等标识对应的载荷不一致")
|
|
}
|
|
return &existing, false, nil
|
|
}
|
|
|
|
// recordAuditEvent 为新的外部交互建立稳定审计关联;审计失败不丢失外部事实。
|
|
func (r *Repository) recordAuditEvent(ctx context.Context, actionCode, integrationID, provider, direction, operation, resourceType string, resourceID, resourceKey, correlationID *string) *uint {
|
|
if r == nil || r.db == nil || r.audit == nil {
|
|
zap.L().Warn("Integration Log 缺少审计关联", zap.String("integration_id", integrationID), zap.String("reason", "审计 Writer 未配置"))
|
|
return nil
|
|
}
|
|
value := auditcontext.From(ctx)
|
|
if !validAuditOrigin(value.ActorKind, value.ActorID, value.Source) {
|
|
value.ActorKind = constants.AuditActorSystemTask
|
|
value.ActorID = "integration_log"
|
|
value.Source = constants.AuditSourceWorker
|
|
}
|
|
event, err := r.audit.AppendAndGet(ctx, r.db, audit.AppendInput{
|
|
EventID: "integration:" + integrationID,
|
|
ActionCode: actionCode, Summary: "记录外部交互审计关联",
|
|
Actor: audit.ActorInput{Kind: value.ActorKind, ID: value.ActorID, Name: value.ActorName, ShopID: value.ActorShopID, EnterpriseID: value.ActorEnterpriseID},
|
|
Source: value.Source, ScopeType: constants.AuditScopePlatform,
|
|
Result: constants.AuditResultSuccess, RequestID: textValue(correlationID), CorrelationID: textValue(correlationID),
|
|
Resources: []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIntegrationLog, Key: integrationID, DisplayName: operation,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleWorkerIntegration,
|
|
IdentitySnapshot: map[string]any{
|
|
"integration_id": integrationID, "provider": provider, "direction": direction, "operation": operation,
|
|
"resource_type": resourceType, "resource_id": textValue(resourceID), "resource_key": textValue(resourceKey), "correlation_id": textValue(correlationID),
|
|
},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
zap.L().Warn("Integration Log 缺少审计关联", zap.String("integration_id", integrationID), zap.Error(err))
|
|
return nil
|
|
}
|
|
return &event.ID
|
|
}
|
|
|
|
func (r *Repository) auditEventIDForSeries(ctx context.Context, triggerSeries string) *uint {
|
|
if r == nil || r.db == nil || triggerSeries == "" {
|
|
return nil
|
|
}
|
|
var log model.IntegrationLog
|
|
if err := r.db.WithContext(ctx).Where("trigger_series = ? AND audit_event_id IS NOT NULL", triggerSeries).Order("attempt DESC").First(&log).Error; err != nil {
|
|
return nil
|
|
}
|
|
return log.AuditEventID
|
|
}
|
|
|
|
func validAuditOrigin(actorKind, actorID, source string) bool {
|
|
if actorID == "" {
|
|
return false
|
|
}
|
|
switch source {
|
|
case constants.AuditSourceAdminAPI:
|
|
return actorKind == constants.AuditActorAccount
|
|
case constants.AuditSourcePersonalAPI:
|
|
return actorKind == constants.AuditActorPersonalCustomer
|
|
case constants.AuditSourceOpenAPI:
|
|
return actorKind == constants.AuditActorOpenAPI
|
|
case constants.AuditSourceCallback:
|
|
return actorKind == constants.AuditActorExternalSystem
|
|
case constants.AuditSourceScheduler:
|
|
return actorKind == constants.AuditActorScheduledJob
|
|
case constants.AuditSourceWorker:
|
|
return actorKind == constants.AuditActorSystemTask
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func textValue(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
// ClaimExpiredInboundPending 原子认领已超过处理租约的入站 pending 记录。
|
|
func (r *Repository) ClaimExpiredInboundPending(ctx context.Context, integrationID string, lease time.Duration) (bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return false, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
|
|
}
|
|
if strings.TrimSpace(integrationID) == "" || lease <= 0 {
|
|
return false, pkgerrors.New(pkgerrors.CodeInvalidParam, "入站 Integration Log 恢复参数无效")
|
|
}
|
|
now := r.now().UTC()
|
|
result := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
|
|
Where("integration_id = ? AND direction = ? AND result = ? AND (started_at IS NULL OR started_at <= ?)",
|
|
integrationID, constants.IntegrationDirectionInbound, constants.IntegrationResultPending, now.Add(-lease)).
|
|
Updates(map[string]any{
|
|
"started_at": now,
|
|
"attempt": gorm.Expr("attempt + 1"),
|
|
"updated_at": now,
|
|
})
|
|
if result.Error != nil {
|
|
return false, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, result.Error, "认领待恢复入站 Integration Log 失败")
|
|
}
|
|
return result.RowsAffected == 1, nil
|
|
}
|
|
|
|
// ClaimFailedInbound 原子认领失败的入站回调并恢复为待处理状态。
|
|
func (r *Repository) ClaimFailedInbound(ctx context.Context, integrationID string) (bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return false, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Integration Log 数据库未配置")
|
|
}
|
|
if strings.TrimSpace(integrationID) == "" {
|
|
return false, pkgerrors.New(pkgerrors.CodeInvalidParam, "入站 Integration Log 恢复参数无效")
|
|
}
|
|
now := r.now().UTC()
|
|
result := r.db.WithContext(ctx).Model(&model.IntegrationLog{}).
|
|
Where("integration_id = ? AND direction = ? AND result = ?", integrationID, constants.IntegrationDirectionInbound, constants.IntegrationResultFailed).
|
|
Updates(map[string]any{
|
|
"result": constants.IntegrationResultPending, "started_at": now,
|
|
"attempt": gorm.Expr("attempt + 1"), "updated_at": now,
|
|
"http_status": nil, "provider_code": nil, "provider_message": nil,
|
|
"response_summary": nil, "duration_ms": 0, "state_changed": false, "recovery_strategy": nil,
|
|
})
|
|
if result.Error != nil {
|
|
return false, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, result.Error, "认领失败入站 Integration Log 失败")
|
|
}
|
|
return result.RowsAffected == 1, nil
|
|
}
|
|
|
|
func validateAttempt(input Attempt) error {
|
|
if input.Provider == "" || input.Operation == "" {
|
|
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 提供方和操作不能为空")
|
|
}
|
|
if input.Direction != constants.IntegrationDirectionInbound && input.Direction != constants.IntegrationDirectionOutbound {
|
|
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 方向无效")
|
|
}
|
|
if input.InitialResult != "" && input.InitialResult != constants.IntegrationResultPending && !isTerminalResult(input.InitialResult) {
|
|
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 初始结果无效")
|
|
}
|
|
if !validGeneratedString(input.IntegrationID, constants.IntegrationIDMaxLength) ||
|
|
!validOptionalString(input.TriggerSeries, constants.IntegrationTriggerSeriesMaxLength) ||
|
|
!validOptionalString(input.CorrelationID, constants.IntegrationCorrelationIDMaxLength) ||
|
|
!validOptionalString(input.ResourceID, constants.IntegrationResourceIDMaxLength) ||
|
|
!validOptionalString(input.ResourceKey, constants.IntegrationResourceKeyMaxLength) {
|
|
return pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 链路或资源标识无效")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validGeneratedString(value string, maxLength int) bool {
|
|
return value == "" || validRequiredString(value, maxLength)
|
|
}
|
|
|
|
func validRequiredString(value string, maxLength int) bool {
|
|
return strings.TrimSpace(value) == value && value != "" && utf8.RuneCountInString(value) <= maxLength
|
|
}
|
|
|
|
func validOptionalString(value *string, maxLength int) bool {
|
|
return value == nil || validRequiredString(*value, maxLength)
|
|
}
|
|
|
|
func isTerminalResult(result string) bool {
|
|
switch result {
|
|
case constants.IntegrationResultSuccess, constants.IntegrationResultFailed, constants.IntegrationResultUnknown,
|
|
constants.IntegrationResultNotFound, constants.IntegrationResultInvalidPayload, constants.IntegrationResultIgnored,
|
|
constants.IntegrationResultConflict,
|
|
constants.IntegrationResultMerged, constants.IntegrationResultRateLimited, constants.IntegrationResultCompleted,
|
|
constants.IntegrationResultCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isUnsentResult(result string) bool {
|
|
switch result {
|
|
case constants.IntegrationResultIgnored, constants.IntegrationResultMerged, constants.IntegrationResultRateLimited,
|
|
constants.IntegrationResultCompleted, constants.IntegrationResultCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func marshalSummary(value any) (datatypes.JSON, error) {
|
|
if value == nil {
|
|
return nil, nil
|
|
}
|
|
encoded, err := sanitizer.MarshalSummary(value)
|
|
return datatypes.JSON(encoded), err
|
|
}
|
|
|
|
func optionalString(value string) *string {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func sanitizedOptionalText(value *string) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
sanitized := sanitizer.SanitizeText(*value)
|
|
return &sanitized
|
|
}
|