refactor: share admin state validation
This commit is contained in:
parent
2dbfe2f742
commit
c5311aa9c0
@ -51,6 +51,9 @@ Repository。
|
|||||||
行为契约。Controller Admin 应用层只负责 DTO 映射、配置解析/校验和运行态发布,
|
行为契约。Controller Admin 应用层只负责 DTO 映射、配置解析/校验和运行态发布,
|
||||||
不自行拼装数据库事务。
|
不自行拼装数据库事务。
|
||||||
|
|
||||||
|
所有命令和查询在领域类型上提供公用 `Validate()`;Memory 与 PostgreSQL
|
||||||
|
Adapter 必须在访问存储前调用同一方法,不复制名称、引用、分页或租约校验。
|
||||||
|
|
||||||
### 全局修订
|
### 全局修订
|
||||||
|
|
||||||
每次产生管理状态变化时分配单调递增的全局 `revision`:
|
每次产生管理状态变化时分配单调递增的全局 `revision`:
|
||||||
|
|||||||
@ -194,7 +194,8 @@ type AcknowledgeCommand struct {
|
|||||||
EventIDs []uint64
|
EventIDs []uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSetUpstreamCommand(command SetUpstreamCommand) error {
|
// Validate checks whether the command is safe for every Store adapter to execute.
|
||||||
|
func (command SetUpstreamCommand) Validate() error {
|
||||||
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
||||||
!validIdentifier(command.Name) {
|
!validIdentifier(command.Name) {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
@ -202,7 +203,8 @@ func validateSetUpstreamCommand(command SetUpstreamCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSwitchRoutingCommand(command SwitchRoutingCommand) error {
|
// Validate checks whether the command is safe for every Store adapter to execute.
|
||||||
|
func (command SwitchRoutingCommand) Validate() error {
|
||||||
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
||||||
!validIdentifier(command.Name) || !validIdentifier(command.ExpectedCurrent) ||
|
!validIdentifier(command.Name) || !validIdentifier(command.ExpectedCurrent) ||
|
||||||
!validIdentifier(command.Target) || !validOptionalText(command.Reason, MaxReasonBytes) {
|
!validIdentifier(command.Target) || !validOptionalText(command.Reason, MaxReasonBytes) {
|
||||||
@ -211,7 +213,8 @@ func validateSwitchRoutingCommand(command SwitchRoutingCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCommitConfigCommand(command CommitConfigCommand) error {
|
// Validate checks the complete management snapshot and all of its references.
|
||||||
|
func (command CommitConfigCommand) Validate() error {
|
||||||
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
|
||||||
!validRequiredText(command.ConfigVersion, MaxIdentifierBytes) ||
|
!validRequiredText(command.ConfigVersion, MaxIdentifierBytes) ||
|
||||||
!validSHA256(command.Checksum) || !validRequiredText(command.Source, MaxSourceBytes) ||
|
!validSHA256(command.Checksum) || !validRequiredText(command.Source, MaxSourceBytes) ||
|
||||||
@ -260,7 +263,8 @@ func validateCommitConfigCommand(command CommitConfigCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateClaimCommand(command ClaimCommand) error {
|
// Validate checks the bounded outbox claim request.
|
||||||
|
func (command ClaimCommand) Validate() error {
|
||||||
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || command.Limit <= 0 ||
|
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || command.Limit <= 0 ||
|
||||||
command.Limit > MaxPageSize || command.Lease <= 0 {
|
command.Limit > MaxPageSize || command.Lease <= 0 {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
@ -268,7 +272,8 @@ func validateClaimCommand(command ClaimCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAcknowledgeCommand(command AcknowledgeCommand) error {
|
// Validate checks the bounded, duplicate-free outbox acknowledgement request.
|
||||||
|
func (command AcknowledgeCommand) Validate() error {
|
||||||
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || len(command.EventIDs) == 0 ||
|
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || len(command.EventIDs) == 0 ||
|
||||||
len(command.EventIDs) > MaxPageSize {
|
len(command.EventIDs) > MaxPageSize {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
@ -286,7 +291,8 @@ func validateAcknowledgeCommand(command AcknowledgeCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAuditQuery(query AuditQuery) error {
|
// Validate checks the bounded audit pagination request.
|
||||||
|
func (query AuditQuery) Validate() error {
|
||||||
if query.Limit <= 0 || query.Limit > MaxPageSize {
|
if query.Limit <= 0 || query.Limit > MaxPageSize {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (store *MemoryStore) CommitConfig(ctx context.Context, command CommitConfig
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if store == nil || validateCommitConfigCommand(command) != nil {
|
if store == nil || command.Validate() != nil {
|
||||||
return result, ErrInvalidCommand
|
return result, ErrInvalidCommand
|
||||||
}
|
}
|
||||||
command = cloneCommitConfigCommand(command)
|
command = cloneCommitConfigCommand(command)
|
||||||
@ -109,7 +109,7 @@ func (store *MemoryStore) SetUpstreamEnabled(ctx context.Context, command SetUps
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if store == nil || validateSetUpstreamCommand(command) != nil {
|
if store == nil || command.Validate() != nil {
|
||||||
return result, ErrInvalidCommand
|
return result, ErrInvalidCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ func (store *MemoryStore) SwitchRouting(ctx context.Context, command SwitchRouti
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if store == nil || validateSwitchRoutingCommand(command) != nil {
|
if store == nil || command.Validate() != nil {
|
||||||
return result, ErrInvalidCommand
|
return result, ErrInvalidCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ func (store *MemoryStore) ReadAudit(ctx context.Context, query AuditQuery) ([]Au
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if store == nil || validateAuditQuery(query) != nil {
|
if store == nil || query.Validate() != nil {
|
||||||
return nil, ErrInvalidCommand
|
return nil, ErrInvalidCommand
|
||||||
}
|
}
|
||||||
store.mu.Lock()
|
store.mu.Lock()
|
||||||
@ -273,7 +273,7 @@ func (store *MemoryStore) Claim(ctx context.Context, command ClaimCommand) ([]Ev
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if store == nil || validateClaimCommand(command) != nil {
|
if store == nil || command.Validate() != nil {
|
||||||
return nil, ErrInvalidCommand
|
return nil, ErrInvalidCommand
|
||||||
}
|
}
|
||||||
store.mu.Lock()
|
store.mu.Lock()
|
||||||
@ -303,7 +303,7 @@ func (store *MemoryStore) Acknowledge(ctx context.Context, command AcknowledgeCo
|
|||||||
if err := contextError(ctx); err != nil {
|
if err := contextError(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if store == nil || validateAcknowledgeCommand(command) != nil {
|
if store == nil || command.Validate() != nil {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
}
|
}
|
||||||
store.mu.Lock()
|
store.mu.Lock()
|
||||||
|
|||||||
@ -15,8 +15,8 @@ func TestValidateSetUpstreamCommand(t *testing.T) {
|
|||||||
RequestID: "req-upstream", Actor: Actor{ID: "admin-a", SourceIP: "192.0.2.10"},
|
RequestID: "req-upstream", Actor: Actor{ID: "admin-a", SourceIP: "192.0.2.10"},
|
||||||
OccurredAt: now, Name: "provider-a", Enabled: true,
|
OccurredAt: now, Name: "provider-a", Enabled: true,
|
||||||
}
|
}
|
||||||
if err := validateSetUpstreamCommand(valid); err != nil {
|
if err := valid.Validate(); err != nil {
|
||||||
t.Fatalf("validateSetUpstreamCommand(valid): %v", err)
|
t.Fatalf("SetUpstreamCommand.Validate(valid): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -35,8 +35,8 @@ func TestValidateSetUpstreamCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
command := valid
|
command := valid
|
||||||
test.mutate(&command)
|
test.mutate(&command)
|
||||||
if err := validateSetUpstreamCommand(command); !errors.Is(err, ErrInvalidCommand) {
|
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
t.Fatalf("validateSetUpstreamCommand() error = %v, want ErrInvalidCommand", err)
|
t.Fatalf("SetUpstreamCommand.Validate() error = %v, want ErrInvalidCommand", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -49,8 +49,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
|
|||||||
RequestID: "req-switch", Actor: Actor{ID: "admin-a"}, OccurredAt: now,
|
RequestID: "req-switch", Actor: Actor{ID: "admin-a"}, OccurredAt: now,
|
||||||
Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b", Reason: "capacity",
|
Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b", Reason: "capacity",
|
||||||
}
|
}
|
||||||
if err := validateSwitchRoutingCommand(valid); err != nil {
|
if err := valid.Validate(); err != nil {
|
||||||
t.Fatalf("validateSwitchRoutingCommand(valid): %v", err)
|
t.Fatalf("SwitchRoutingCommand.Validate(valid): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@ -67,8 +67,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
command := valid
|
command := valid
|
||||||
test.mutate(&command)
|
test.mutate(&command)
|
||||||
if err := validateSwitchRoutingCommand(command); !errors.Is(err, ErrInvalidCommand) {
|
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
t.Fatalf("validateSwitchRoutingCommand() error = %v, want ErrInvalidCommand", err)
|
t.Fatalf("SwitchRoutingCommand.Validate() error = %v, want ErrInvalidCommand", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -77,8 +77,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
|
|||||||
func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
|
func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
valid := validCommitConfigCommand()
|
valid := validCommitConfigCommand()
|
||||||
if err := validateCommitConfigCommand(valid); err != nil {
|
if err := valid.Validate(); err != nil {
|
||||||
t.Fatalf("validateCommitConfigCommand(valid): %v", err)
|
t.Fatalf("CommitConfigCommand.Validate(valid): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@ -102,8 +102,8 @@ func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
command := cloneCommitConfigCommand(valid)
|
command := cloneCommitConfigCommand(valid)
|
||||||
test.mutate(&command)
|
test.mutate(&command)
|
||||||
if err := validateCommitConfigCommand(command); !errors.Is(err, ErrInvalidCommand) {
|
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
t.Fatalf("validateCommitConfigCommand() error = %v, want ErrInvalidCommand", err)
|
t.Fatalf("CommitConfigCommand.Validate() error = %v, want ErrInvalidCommand", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -112,15 +112,15 @@ func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
|
|||||||
func TestValidateOutboxCommands(t *testing.T) {
|
func TestValidateOutboxCommands(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||||
if err := validateClaimCommand(ClaimCommand{
|
if err := (ClaimCommand{
|
||||||
ConsumerID: "publisher-a", Now: now, Limit: 100, Lease: time.Minute,
|
ConsumerID: "publisher-a", Now: now, Limit: 100, Lease: time.Minute,
|
||||||
}); err != nil {
|
}).Validate(); err != nil {
|
||||||
t.Fatalf("validateClaimCommand(valid): %v", err)
|
t.Fatalf("ClaimCommand.Validate(valid): %v", err)
|
||||||
}
|
}
|
||||||
if err := validateAcknowledgeCommand(AcknowledgeCommand{
|
if err := (AcknowledgeCommand{
|
||||||
ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 2},
|
ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 2},
|
||||||
}); err != nil {
|
}).Validate(); err != nil {
|
||||||
t.Fatalf("validateAcknowledgeCommand(valid): %v", err)
|
t.Fatalf("AcknowledgeCommand.Validate(valid): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidClaims := []ClaimCommand{
|
invalidClaims := []ClaimCommand{
|
||||||
@ -131,8 +131,8 @@ func TestValidateOutboxCommands(t *testing.T) {
|
|||||||
{ConsumerID: "publisher-a", Now: now, Limit: 1},
|
{ConsumerID: "publisher-a", Now: now, Limit: 1},
|
||||||
}
|
}
|
||||||
for _, command := range invalidClaims {
|
for _, command := range invalidClaims {
|
||||||
if err := validateClaimCommand(command); !errors.Is(err, ErrInvalidCommand) {
|
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
t.Fatalf("validateClaimCommand(%+v) error = %v", command, err)
|
t.Fatalf("ClaimCommand.Validate(%+v) error = %v", command, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,8 +144,20 @@ func TestValidateOutboxCommands(t *testing.T) {
|
|||||||
{ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 1}},
|
{ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 1}},
|
||||||
}
|
}
|
||||||
for _, command := range invalidAcks {
|
for _, command := range invalidAcks {
|
||||||
if err := validateAcknowledgeCommand(command); !errors.Is(err, ErrInvalidCommand) {
|
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
t.Fatalf("validateAcknowledgeCommand(%+v) error = %v", command, err)
|
t.Fatalf("AcknowledgeCommand.Validate(%+v) error = %v", command, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuditQueryValidate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if err := (AuditQuery{Limit: 10}).Validate(); err != nil {
|
||||||
|
t.Fatalf("AuditQuery.Validate(valid): %v", err)
|
||||||
|
}
|
||||||
|
for _, query := range []AuditQuery{{}, {Limit: -1}, {Limit: MaxPageSize + 1}} {
|
||||||
|
if err := query.Validate(); !errors.Is(err, ErrInvalidCommand) {
|
||||||
|
t.Fatalf("AuditQuery.Validate(%+v) error = %v, want ErrInvalidCommand", query, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## 2026-07-29
|
## 2026-07-29
|
||||||
|
|
||||||
|
- `adminstate` 六类命令/查询已封装公用 `Validate()`,MemoryStore 改为统一复用;
|
||||||
|
后续 pgx Adapter 不再重复实现名称、引用、分页和 Outbox 租约输入校验。
|
||||||
|
- 一次命令装配审计误用了不存在的 `controller/distribution/service.go`;实际领域
|
||||||
|
服务位于 `controller/extraction`,Distribution 包当前只负责 HTTP Handler。
|
||||||
- 根据对话最终定稿统一 Sequential:至少两个 Upstream,省略 `endBehavior` 时
|
- 根据对话最终定稿统一 Sequential:至少两个 Upstream,省略 `endBehavior` 时
|
||||||
默认 `stop`;新增领域和严格配置回归测试。disabled candidate、跨实例游标和
|
默认 `stop`;新增领域和严格配置回归测试。disabled candidate、跨实例游标和
|
||||||
`onUnavailable` 请求链仍未提前标记完成。
|
`onUnavailable` 请求链仍未提前标记完成。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user