feat: add scoped workflow instance queries and SDK pagination
This commit is contained in:
83
weave/instance/query.go
Normal file
83
weave/instance/query.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
weave "git.yuntuo.hk/k2safe/orbit/weave/model"
|
||||
)
|
||||
|
||||
// ListInput 在认证命名空间内筛选实例;所有队列权限必须覆盖整个固定定义版本。
|
||||
type ListInput struct {
|
||||
DefinitionID core.ID // DefinitionID 可选定义身份,未传入时跨已授权定义查询。
|
||||
Version string // Version 可选固定发布版本名,仅在指定定义时使用。
|
||||
State weave.InstanceState // State 可选执行状态,与暂停标志独立。
|
||||
Paused *bool // Paused nil不筛选;false明确筛选未暂停实例。
|
||||
BusinessKey string // BusinessKey 非空时精确匹配,包含尾部空格;空表示不筛选。
|
||||
CreatedFrom *time.Time // CreatedFrom 可选创建时间下界,包含,UTC微秒精度。
|
||||
CreatedBefore *time.Time // CreatedBefore 可选创建时间上界,不包含。
|
||||
Limit int // Limit 默认20,上限100。
|
||||
Cursor string // Cursor 服务端返回的位置,绑定命名空间、全部筛选及队列权限范围。
|
||||
}
|
||||
|
||||
// Summary 仅返回实例元数据,不聚合业务输入输出、定义或策略快照。
|
||||
type Summary struct {
|
||||
ID core.ID `json:"id"` // ID 实例身份。
|
||||
NamespaceID core.ID `json:"namespace_id"` // NamespaceID 授权及数据隔离范围。
|
||||
DefinitionID core.ID `json:"definition_id"` // DefinitionID 稳定定义身份。
|
||||
DefinitionVersionID core.ID `json:"definition_version_id"` // DefinitionVersionID 固定版本记录身份。
|
||||
Version string `json:"version"` // Version 启动时固定的发布版本名。
|
||||
State weave.InstanceState `json:"state"` // State 执行状态,受理不代表业务成功。
|
||||
Revision uint64 `json:"revision,string"` // Revision 实例并发版本。
|
||||
Paused bool `json:"paused"` // Paused 独立暂停标志,不表示在途工作已停止。
|
||||
ControlRevision uint64 `json:"control_revision,string"` // ControlRevision 控制操作并发版本。
|
||||
BusinessKey string `json:"business_key"` // BusinessKey 原始追踪键。
|
||||
DeadlineAt time.Time `json:"deadline_at"` // DeadlineAt 数据库时间确定的固定总截止。
|
||||
CancelRequestedAt *time.Time `json:"cancel_requested_at,omitempty"` // CancelRequestedAt 已保存的取消意图时间。
|
||||
StopKind *string `json:"stop_kind,omitempty"` // StopKind 首次停止原因CANCEL或FAILURE;核查不会清除。
|
||||
StopRequestedAt *time.Time `json:"stop_requested_at,omitempty"` // StopRequestedAt 首次停止意图时间,后续操作不会延长。
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"` // CompletedAt 终结时间,非终态为空。
|
||||
CreatedAt time.Time `json:"created_at"` // CreatedAt UTC受理时间。
|
||||
UpdatedAt time.Time `json:"updated_at"` // UpdatedAt 最近状态更新时间。
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Items []Summary `json:"items"` // Items 按创建时间及ID倒序的授权摘要;空列表为[]。
|
||||
NextCursor string `json:"next_cursor,omitempty"` // NextCursor 沿相同筛选与权限继续使用,空表示本次无下一页。
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, t auth.Token, in ListInput) (Page, error) {
|
||||
if in.Limit == 0 {
|
||||
in.Limit = 20
|
||||
}
|
||||
if in.Limit < 1 || in.Limit > 100 || len(in.Cursor) > 1024 || in.DefinitionID != "" && in.DefinitionID.Validate() != nil || in.Version != "" && (in.DefinitionID == "" || !versionName(in.Version)) {
|
||||
return Page{}, fault.Invalid("invalid workflow query definition, version or page")
|
||||
}
|
||||
switch in.State {
|
||||
case "", "RUNNING", "WAITING", "CANCELLING", "NEEDS_REVIEW", "SUCCEEDED", "FAILED", "CANCELLED":
|
||||
default:
|
||||
return Page{}, fault.Invalid("invalid workflow state")
|
||||
}
|
||||
if !utf8.ValidString(in.BusinessKey) || utf8.RuneCountInString(in.BusinessKey) > 191 || strings.ContainsRune(in.BusinessKey, 0) {
|
||||
return Page{}, fault.Invalid("invalid workflow business key filter")
|
||||
}
|
||||
for _, f := range []**time.Time{&in.CreatedFrom, &in.CreatedBefore} {
|
||||
if *f == nil {
|
||||
continue
|
||||
}
|
||||
v := (*f).UTC()
|
||||
if v.Year() < 2000 || v.Year() > 9998 || v.Nanosecond()%1000 != 0 {
|
||||
return Page{}, fault.Invalid("invalid workflow creation time bound")
|
||||
}
|
||||
*f = &v
|
||||
}
|
||||
if in.CreatedFrom != nil && in.CreatedBefore != nil && !in.CreatedFrom.Before(*in.CreatedBefore) {
|
||||
return Page{}, fault.Invalid("created_from must be before created_before")
|
||||
}
|
||||
return s.store.List(ctx, t, in)
|
||||
}
|
||||
27
weave/instance/query_test.go
Normal file
27
weave/instance/query_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
)
|
||||
|
||||
func TestListBoundary(t *testing.T) {
|
||||
store := &captureStore{}
|
||||
api := New(store)
|
||||
if _, err := api.List(context.Background(), auth.Token{}, ListInput{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
for _, in := range []ListInput{{Limit: -1}, {Limit: 101}, {Cursor: strings.Repeat("x", 1025)}, {DefinitionID: "bad"}, {Version: "v1"}, {DefinitionID: "11111111111111111111111111111111", Version: "../v1"}, {State: "STOPPED"}, {BusinessKey: "\xff"}, {BusinessKey: "nul\x00"}, {BusinessKey: strings.Repeat("中", 192)}, {CreatedFrom: &now, CreatedBefore: &now}} {
|
||||
if _, err := api.List(context.Background(), auth.Token{}, in); err == nil {
|
||||
t.Fatal("invalid query accepted", in)
|
||||
}
|
||||
}
|
||||
if store.calls != 1 {
|
||||
t.Fatal("invalid query reached store")
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ type StepsView struct {
|
||||
Steps []StepView `json:"steps"` // Steps 按键排序,最多128个,不含内部根检查点。
|
||||
}
|
||||
type Store interface {
|
||||
List(context.Context, auth.Token, ListInput) (Page, error)
|
||||
ListActivations(context.Context, auth.Token, core.ID, string, service.HistoryPageInput) (ActivationPage, error)
|
||||
GetActivation(context.Context, auth.Token, core.ID, string, core.ID) (ActivationDetail, error)
|
||||
ListPollRounds(context.Context, auth.Token, core.ID, string, core.ID, service.HistoryPageInput) (PollRoundPage, error)
|
||||
|
||||
@@ -84,3 +84,8 @@ func (s *captureStore) ListPollRounds(context.Context, auth.Token, core.ID, stri
|
||||
s.calls++
|
||||
return PollRoundPage{}, nil
|
||||
}
|
||||
|
||||
func (s *captureStore) List(context.Context, auth.Token, ListInput) (Page, error) {
|
||||
s.calls++
|
||||
return Page{}, nil
|
||||
}
|
||||
|
||||
20
weave/model/version_scope.go
Normal file
20
weave/model/version_scope.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
)
|
||||
|
||||
// VersionScope 是固定流程版本的队列查询索引;不能替代原定义校验或授予执行权限。
|
||||
// 对应weave_version_scopes,不使用外键;发布与本索引在同一事务提交。
|
||||
type VersionScope struct {
|
||||
NamespaceID core.ID `db:"namespace_id"` // NamespaceID 命名空间,与版本共同构成唯一身份及授权边界。
|
||||
DefinitionVersionID core.ID `db:"definition_version_id"` // DefinitionVersionID 固定定义版本标识,包含未执行分支的全部队列。
|
||||
DefinitionID core.ID `db:"definition_id"` // DefinitionID 所属定义,查询必须与版本和实例共同核对。
|
||||
ContentDigest core.Digest `db:"content_digest"` // ContentDigest 原不可变定义摘要,不能用其他版本的范围授权。
|
||||
QueueIDs core.JSON `db:"queue_ids"` // QueueIDs 去重的32位队列标识JSON数组,最多128项;空数组表示流程不执行任务,顺序无语义。
|
||||
CreatedAt time.Time `db:"created_at"` // CreatedAt 索引建立时间,UTC;不改变原版本发布时间或实例截止。
|
||||
}
|
||||
|
||||
func (VersionScope) TableName() string { return "weave_version_scopes" }
|
||||
Reference in New Issue
Block a user