379 lines
17 KiB
Go
379 lines
17 KiB
Go
package diagnostics
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/audit"
|
||
|
|
"data-terminal-cloud/internal/concurrency"
|
||
|
|
"data-terminal-cloud/internal/database"
|
||
|
|
"data-terminal-cloud/internal/idempotency"
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
var integrationIssueStatuses = map[string]bool{
|
||
|
|
"OPEN": true, "IN_PROGRESS": true, "RESOLVED": true, "VERIFIED": true, "CLOSED": true,
|
||
|
|
}
|
||
|
|
|
||
|
|
var integrationIssueSeverities = map[string]bool{
|
||
|
|
"LOW": true, "MEDIUM": true, "HIGH": true, "CRITICAL": true,
|
||
|
|
}
|
||
|
|
|
||
|
|
type IntegrationIssue struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
TenantID string `json:"tenant_id"`
|
||
|
|
Phenomenon string `json:"phenomenon"`
|
||
|
|
Severity string `json:"severity"`
|
||
|
|
DeviceID string `json:"device_id,omitempty"`
|
||
|
|
ConversationID string `json:"conversation_id,omitempty"`
|
||
|
|
OccurredAt time.Time `json:"occurred_at"`
|
||
|
|
RequestID string `json:"request_id,omitempty"`
|
||
|
|
ResponsibleParty string `json:"responsible_party"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
ResolutionResult string `json:"resolution_result,omitempty"`
|
||
|
|
RootCause string `json:"root_cause,omitempty"`
|
||
|
|
FixedVersion string `json:"fixed_version,omitempty"`
|
||
|
|
RegressionConclusion string `json:"regression_conclusion,omitempty"`
|
||
|
|
RegressionEvidence string `json:"regression_evidence,omitempty"`
|
||
|
|
CreatedBy string `json:"created_by"`
|
||
|
|
UpdatedBy string `json:"updated_by"`
|
||
|
|
Version int64 `json:"version"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
UpdatedAt time.Time `json:"updated_at"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type CreateIntegrationIssueInput struct {
|
||
|
|
Phenomenon string `json:"phenomenon"`
|
||
|
|
Severity string `json:"severity"`
|
||
|
|
DeviceID string `json:"device_id,omitempty"`
|
||
|
|
ConversationID string `json:"conversation_id,omitempty"`
|
||
|
|
OccurredAt time.Time `json:"occurred_at"`
|
||
|
|
RequestID string `json:"request_id,omitempty"`
|
||
|
|
ResponsibleParty string `json:"responsible_party"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type UpdateIntegrationIssueInput struct {
|
||
|
|
Status *string `json:"status,omitempty"`
|
||
|
|
Severity *string `json:"severity,omitempty"`
|
||
|
|
ResponsibleParty *string `json:"responsible_party,omitempty"`
|
||
|
|
ResolutionResult *string `json:"resolution_result,omitempty"`
|
||
|
|
RootCause *string `json:"root_cause,omitempty"`
|
||
|
|
FixedVersion *string `json:"fixed_version,omitempty"`
|
||
|
|
RegressionConclusion *string `json:"regression_conclusion,omitempty"`
|
||
|
|
RegressionEvidence *string `json:"regression_evidence,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type IntegrationIssueSummary struct {
|
||
|
|
Total int64 `json:"total"`
|
||
|
|
Open int64 `json:"open"`
|
||
|
|
CriticalOpen int64 `json:"critical_open"`
|
||
|
|
HighOpen int64 `json:"high_open"`
|
||
|
|
ReleaseBlocking int64 `json:"release_blocking"`
|
||
|
|
ReleaseGateSatisfied bool `json:"release_gate_satisfied"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CreateIntegrationIssue(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID string, input CreateIntegrationIssueInput) (IntegrationIssue, bool, error) {
|
||
|
|
normalizeIntegrationIssueCreate(&input)
|
||
|
|
if !validIntegrationIssueCreate(input) {
|
||
|
|
return IntegrationIssue{}, false, ErrInvalid
|
||
|
|
}
|
||
|
|
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
|
||
|
|
if err != nil {
|
||
|
|
return IntegrationIssue{}, false, err
|
||
|
|
}
|
||
|
|
requestBody, _ := json.Marshal(input)
|
||
|
|
result, err := s.idem.Execute(ctx, idempotency.Request{
|
||
|
|
TenantID: ¤tActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID,
|
||
|
|
Scope: "POST /api/v1/ops/integration-issues", Key: key,
|
||
|
|
RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour,
|
||
|
|
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
|
||
|
|
deviceID, conversationID, resolveErr := resolveIntegrationIssueReferences(operationContext, tx, currentActor.tenantID, input.DeviceID, input.ConversationID)
|
||
|
|
if resolveErr != nil {
|
||
|
|
return idempotency.Result{}, resolveErr
|
||
|
|
}
|
||
|
|
publicID := uuid.NewString()
|
||
|
|
var internalID string
|
||
|
|
if queryErr := tx.QueryRow(operationContext, `
|
||
|
|
INSERT INTO integration_issue(public_id,tenant_id,phenomenon,severity,device_id,conversation_id,occurred_at,request_id,responsible_party,created_by,updated_by)
|
||
|
|
VALUES($1::uuid,$2::uuid,$3,$4,$5::uuid,$6::uuid,$7,NULLIF($8,''),$9,$10::uuid,$10::uuid)
|
||
|
|
RETURNING id::text`, publicID, currentActor.tenantID, input.Phenomenon, input.Severity, deviceID, conversationID, input.OccurredAt, input.RequestID, input.ResponsibleParty, currentActor.userID).Scan(&internalID); queryErr != nil {
|
||
|
|
return idempotency.Result{}, queryErr
|
||
|
|
}
|
||
|
|
resourceID := internalID
|
||
|
|
if auditErr := s.audit.Record(operationContext, tx, audit.Event{
|
||
|
|
TenantID: ¤tActor.tenantID, ActorType: "user", ActorPublicID: ¤tActor.userPublicID,
|
||
|
|
Action: "ops.integration_issue.create", ResourceType: "integration_issue", ResourceID: &resourceID,
|
||
|
|
RequestID: requestID, Purpose: "记录联调问题", Result: "success",
|
||
|
|
Detail: map[string]any{"issue_id": publicID, "severity": input.Severity, "device_id": input.DeviceID, "conversation_id": input.ConversationID, "responsible_party": input.ResponsibleParty},
|
||
|
|
}); auditErr != nil {
|
||
|
|
return idempotency.Result{}, auditErr
|
||
|
|
}
|
||
|
|
item, queryErr := getIntegrationIssue(operationContext, tx, currentActor.tenantID, publicID, false)
|
||
|
|
if queryErr != nil {
|
||
|
|
return idempotency.Result{}, queryErr
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(item)
|
||
|
|
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return IntegrationIssue{}, false, err
|
||
|
|
}
|
||
|
|
var item IntegrationIssue
|
||
|
|
if err := json.Unmarshal(result.Body, &item); err != nil {
|
||
|
|
return IntegrationIssue{}, false, err
|
||
|
|
}
|
||
|
|
return item, !result.Replayed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) ListIntegrationIssues(ctx context.Context, principal identity.Principal, tenantPublicID, status string) ([]IntegrationIssue, error) {
|
||
|
|
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
status = strings.ToUpper(strings.TrimSpace(status))
|
||
|
|
if status != "" && !integrationIssueStatuses[status] {
|
||
|
|
return nil, ErrInvalid
|
||
|
|
}
|
||
|
|
rows, err := s.pool.Query(ctx, integrationIssueSelect+` WHERE ii.tenant_id=$1::uuid AND (NULLIF($2,'') IS NULL OR ii.status=$2) ORDER BY ii.updated_at DESC,ii.id DESC LIMIT 100`, currentActor.tenantID, status)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
items := []IntegrationIssue{}
|
||
|
|
for rows.Next() {
|
||
|
|
item, scanErr := scanIntegrationIssue(rows)
|
||
|
|
if scanErr != nil {
|
||
|
|
return nil, scanErr
|
||
|
|
}
|
||
|
|
items = append(items, item)
|
||
|
|
}
|
||
|
|
return items, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) SummarizeIntegrationIssues(ctx context.Context, principal identity.Principal, tenantPublicID string) (IntegrationIssueSummary, error) {
|
||
|
|
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
|
||
|
|
if err != nil {
|
||
|
|
return IntegrationIssueSummary{}, err
|
||
|
|
}
|
||
|
|
var summary IntegrationIssueSummary
|
||
|
|
err = s.pool.QueryRow(ctx, `
|
||
|
|
SELECT count(*),
|
||
|
|
count(*) FILTER (WHERE status <> 'CLOSED'),
|
||
|
|
count(*) FILTER (WHERE severity='CRITICAL' AND status <> 'CLOSED'),
|
||
|
|
count(*) FILTER (WHERE severity='HIGH' AND status <> 'CLOSED'),
|
||
|
|
count(*) FILTER (WHERE severity IN ('CRITICAL','HIGH') AND status <> 'CLOSED')
|
||
|
|
FROM integration_issue
|
||
|
|
WHERE tenant_id=$1::uuid`, currentActor.tenantID).Scan(
|
||
|
|
&summary.Total, &summary.Open, &summary.CriticalOpen, &summary.HighOpen, &summary.ReleaseBlocking,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return IntegrationIssueSummary{}, err
|
||
|
|
}
|
||
|
|
summary.ReleaseGateSatisfied = summary.ReleaseBlocking == 0
|
||
|
|
return summary, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) UpdateIntegrationIssue(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID string, expectedVersion int64, input UpdateIntegrationIssueInput) (IntegrationIssue, error) {
|
||
|
|
if _, err := uuid.Parse(publicID); err != nil {
|
||
|
|
return IntegrationIssue{}, ErrInvalid
|
||
|
|
}
|
||
|
|
normalizeIntegrationIssueUpdate(&input)
|
||
|
|
if input.Status == nil && input.Severity == nil && input.ResponsibleParty == nil && input.ResolutionResult == nil && input.RootCause == nil && input.FixedVersion == nil && input.RegressionConclusion == nil && input.RegressionEvidence == nil {
|
||
|
|
return IntegrationIssue{}, ErrInvalid
|
||
|
|
}
|
||
|
|
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
|
||
|
|
if err != nil {
|
||
|
|
return IntegrationIssue{}, err
|
||
|
|
}
|
||
|
|
var updated IntegrationIssue
|
||
|
|
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
|
||
|
|
current, lockErr := getIntegrationIssue(ctx, tx, currentActor.tenantID, publicID, true)
|
||
|
|
if lockErr != nil {
|
||
|
|
return lockErr
|
||
|
|
}
|
||
|
|
if err := concurrency.RequireVersion(expectedVersion, current.Version); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
previousStatus := current.Status
|
||
|
|
if input.Status != nil {
|
||
|
|
current.Status = *input.Status
|
||
|
|
}
|
||
|
|
if input.Severity != nil {
|
||
|
|
current.Severity = *input.Severity
|
||
|
|
}
|
||
|
|
if input.ResponsibleParty != nil {
|
||
|
|
current.ResponsibleParty = *input.ResponsibleParty
|
||
|
|
}
|
||
|
|
if input.ResolutionResult != nil {
|
||
|
|
current.ResolutionResult = *input.ResolutionResult
|
||
|
|
}
|
||
|
|
if input.RootCause != nil {
|
||
|
|
current.RootCause = *input.RootCause
|
||
|
|
}
|
||
|
|
if input.FixedVersion != nil {
|
||
|
|
current.FixedVersion = *input.FixedVersion
|
||
|
|
}
|
||
|
|
if input.RegressionConclusion != nil {
|
||
|
|
current.RegressionConclusion = *input.RegressionConclusion
|
||
|
|
}
|
||
|
|
if input.RegressionEvidence != nil {
|
||
|
|
current.RegressionEvidence = *input.RegressionEvidence
|
||
|
|
}
|
||
|
|
if !validIntegrationIssueState(current) {
|
||
|
|
return ErrInvalid
|
||
|
|
}
|
||
|
|
command, commandErr := tx.Exec(ctx, `
|
||
|
|
UPDATE integration_issue
|
||
|
|
SET status=$3,severity=$4,responsible_party=$5,resolution_result=NULLIF($6,''),root_cause=NULLIF($7,''),fixed_version=NULLIF($8,''),
|
||
|
|
regression_conclusion=NULLIF($9,''),regression_evidence=NULLIF($10,''),updated_by=$11::uuid,version=version+1,updated_at=now()
|
||
|
|
WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND version=$12`, currentActor.tenantID, publicID, current.Status, current.Severity, current.ResponsibleParty, current.ResolutionResult, current.RootCause, current.FixedVersion, current.RegressionConclusion, current.RegressionEvidence, currentActor.userID, expectedVersion)
|
||
|
|
if commandErr != nil {
|
||
|
|
return commandErr
|
||
|
|
}
|
||
|
|
if command.RowsAffected() != 1 {
|
||
|
|
return concurrency.ErrVersionConflict
|
||
|
|
}
|
||
|
|
var internalID string
|
||
|
|
if queryErr := tx.QueryRow(ctx, `SELECT id::text FROM integration_issue WHERE tenant_id=$1::uuid AND public_id=$2::uuid`, currentActor.tenantID, publicID).Scan(&internalID); queryErr != nil {
|
||
|
|
return queryErr
|
||
|
|
}
|
||
|
|
if auditErr := s.audit.Record(ctx, tx, audit.Event{
|
||
|
|
TenantID: ¤tActor.tenantID, ActorType: "user", ActorPublicID: ¤tActor.userPublicID,
|
||
|
|
Action: "ops.integration_issue.update", ResourceType: "integration_issue", ResourceID: &internalID,
|
||
|
|
RequestID: requestID, Purpose: "维护联调问题闭环", Result: "success",
|
||
|
|
Detail: map[string]any{"issue_id": publicID, "from_status": previousStatus, "to_status": current.Status, "severity": current.Severity, "fixed_version": current.FixedVersion, "responsible_party": current.ResponsibleParty},
|
||
|
|
}); auditErr != nil {
|
||
|
|
return auditErr
|
||
|
|
}
|
||
|
|
updated, lockErr = getIntegrationIssue(ctx, tx, currentActor.tenantID, publicID, false)
|
||
|
|
return lockErr
|
||
|
|
})
|
||
|
|
return updated, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeIntegrationIssueCreate(input *CreateIntegrationIssueInput) {
|
||
|
|
input.Phenomenon = strings.TrimSpace(input.Phenomenon)
|
||
|
|
input.Severity = strings.ToUpper(strings.TrimSpace(input.Severity))
|
||
|
|
input.DeviceID = strings.TrimSpace(input.DeviceID)
|
||
|
|
input.ConversationID = strings.TrimSpace(input.ConversationID)
|
||
|
|
input.RequestID = strings.TrimSpace(input.RequestID)
|
||
|
|
input.ResponsibleParty = strings.TrimSpace(input.ResponsibleParty)
|
||
|
|
}
|
||
|
|
|
||
|
|
func validIntegrationIssueCreate(input CreateIntegrationIssueInput) bool {
|
||
|
|
if input.Phenomenon == "" || len([]rune(input.Phenomenon)) > 2000 || !integrationIssueSeverities[input.Severity] || input.ResponsibleParty == "" || len([]rune(input.ResponsibleParty)) > 120 || input.OccurredAt.IsZero() {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if input.DeviceID != "" {
|
||
|
|
if _, err := uuid.Parse(input.DeviceID); err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if input.ConversationID != "" {
|
||
|
|
if _, err := uuid.Parse(input.ConversationID); err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return input.RequestID == "" || requestIDPattern.MatchString(input.RequestID)
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeIntegrationIssueUpdate(input *UpdateIntegrationIssueInput) {
|
||
|
|
if input.Status != nil {
|
||
|
|
value := strings.ToUpper(strings.TrimSpace(*input.Status))
|
||
|
|
input.Status = &value
|
||
|
|
}
|
||
|
|
if input.Severity != nil {
|
||
|
|
value := strings.ToUpper(strings.TrimSpace(*input.Severity))
|
||
|
|
input.Severity = &value
|
||
|
|
}
|
||
|
|
trimIssueField(&input.ResponsibleParty)
|
||
|
|
trimIssueField(&input.ResolutionResult)
|
||
|
|
trimIssueField(&input.RootCause)
|
||
|
|
trimIssueField(&input.FixedVersion)
|
||
|
|
trimIssueField(&input.RegressionConclusion)
|
||
|
|
trimIssueField(&input.RegressionEvidence)
|
||
|
|
}
|
||
|
|
|
||
|
|
func trimIssueField(value **string) {
|
||
|
|
if *value != nil {
|
||
|
|
trimmed := strings.TrimSpace(**value)
|
||
|
|
*value = &trimmed
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func validIntegrationIssueState(item IntegrationIssue) bool {
|
||
|
|
if !integrationIssueStatuses[item.Status] || !integrationIssueSeverities[item.Severity] || item.ResponsibleParty == "" || len([]rune(item.ResponsibleParty)) > 120 || len([]rune(item.ResolutionResult)) > 4000 || len([]rune(item.RootCause)) > 4000 || len([]rune(item.FixedVersion)) > 128 || len([]rune(item.RegressionConclusion)) > 4000 || len([]rune(item.RegressionEvidence)) > 4000 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (item.Status == "RESOLVED" || item.Status == "VERIFIED" || item.Status == "CLOSED") && (item.ResolutionResult == "" || item.RootCause == "" || item.FixedVersion == "") {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return (item.Status != "VERIFIED" && item.Status != "CLOSED") || (item.RegressionConclusion != "" && item.RegressionEvidence != "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveIntegrationIssueReferences(ctx context.Context, tx pgx.Tx, tenantID, devicePublicID, conversationPublicID string) (*string, *string, error) {
|
||
|
|
var deviceID, conversationID *string
|
||
|
|
if devicePublicID != "" {
|
||
|
|
var value string
|
||
|
|
if err := tx.QueryRow(ctx, `SELECT id::text FROM device WHERE tenant_id=$1::uuid AND public_id=$2::uuid`, tenantID, devicePublicID).Scan(&value); errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil, nil, ErrNotFound
|
||
|
|
} else if err != nil {
|
||
|
|
return nil, nil, err
|
||
|
|
}
|
||
|
|
deviceID = &value
|
||
|
|
}
|
||
|
|
if conversationPublicID != "" {
|
||
|
|
var value, conversationDeviceID string
|
||
|
|
if err := tx.QueryRow(ctx, `SELECT id::text,device_id::text FROM conversation WHERE tenant_id=$1::uuid AND public_id=$2::uuid`, tenantID, conversationPublicID).Scan(&value, &conversationDeviceID); errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil, nil, ErrNotFound
|
||
|
|
} else if err != nil {
|
||
|
|
return nil, nil, err
|
||
|
|
}
|
||
|
|
if deviceID != nil && *deviceID != conversationDeviceID {
|
||
|
|
return nil, nil, ErrInvalid
|
||
|
|
}
|
||
|
|
conversationID = &value
|
||
|
|
}
|
||
|
|
return deviceID, conversationID, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
const integrationIssueSelect = `
|
||
|
|
SELECT ii.public_id::text,t.public_id::text,ii.phenomenon,ii.severity,COALESCE(d.public_id::text,''),COALESCE(c.public_id::text,''),
|
||
|
|
ii.occurred_at,COALESCE(ii.request_id,''),ii.responsible_party,ii.status,
|
||
|
|
COALESCE(ii.resolution_result,''),COALESCE(ii.root_cause,''),COALESCE(ii.fixed_version,''),COALESCE(ii.regression_conclusion,''),COALESCE(ii.regression_evidence,''),
|
||
|
|
creator.public_id::text,updater.public_id::text,ii.version,ii.created_at,ii.updated_at
|
||
|
|
FROM integration_issue ii
|
||
|
|
JOIN tenant t ON t.id=ii.tenant_id
|
||
|
|
JOIN user_account creator ON creator.id=ii.created_by AND creator.tenant_id=ii.tenant_id
|
||
|
|
JOIN user_account updater ON updater.id=ii.updated_by AND updater.tenant_id=ii.tenant_id
|
||
|
|
LEFT JOIN device d ON d.id=ii.device_id AND d.tenant_id=ii.tenant_id
|
||
|
|
LEFT JOIN conversation c ON c.id=ii.conversation_id AND c.tenant_id=ii.tenant_id`
|
||
|
|
|
||
|
|
type integrationIssueScanner interface{ Scan(...any) error }
|
||
|
|
|
||
|
|
func scanIntegrationIssue(row integrationIssueScanner) (IntegrationIssue, error) {
|
||
|
|
var item IntegrationIssue
|
||
|
|
err := row.Scan(&item.ID, &item.TenantID, &item.Phenomenon, &item.Severity, &item.DeviceID, &item.ConversationID, &item.OccurredAt, &item.RequestID, &item.ResponsibleParty, &item.Status, &item.ResolutionResult, &item.RootCause, &item.FixedVersion, &item.RegressionConclusion, &item.RegressionEvidence, &item.CreatedBy, &item.UpdatedBy, &item.Version, &item.CreatedAt, &item.UpdatedAt)
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func getIntegrationIssue(ctx context.Context, query interface {
|
||
|
|
QueryRow(context.Context, string, ...any) pgx.Row
|
||
|
|
}, tenantID, publicID string, lock bool) (IntegrationIssue, error) {
|
||
|
|
queryText := integrationIssueSelect + ` WHERE ii.tenant_id=$1::uuid AND ii.public_id=$2::uuid`
|
||
|
|
if lock {
|
||
|
|
queryText += ` FOR UPDATE OF ii`
|
||
|
|
}
|
||
|
|
item, err := scanIntegrationIssue(query.QueryRow(ctx, queryText, tenantID, publicID))
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return IntegrationIssue{}, ErrNotFound
|
||
|
|
}
|
||
|
|
return item, err
|
||
|
|
}
|