From 7da344be26bc1b31478f105194a412422a226ba9 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Thu, 24 Sep 2020 08:52:10 +0200 Subject: [PATCH] init v2 --- internal/eventstore/v2/aggregate.go | 22 + internal/eventstore/v2/event.go | 54 +++ internal/eventstore/v2/eventstore.go | 12 + internal/eventstore/v2/eventstore_memory.go | 28 ++ internal/eventstore/v2/eventstore_test.go | 24 + internal/eventstore/v2/filter.go | 67 +++ internal/eventstore/v2/filter_test.go | 104 +++++ internal/eventstore/v2/read_model.go | 3 + internal/eventstore/v2/search_query.go | 152 +++++++ internal/eventstore/v2/search_query_test.go | 465 ++++++++++++++++++++ internal/eventstore/v2/version.go | 18 + internal/eventstore/v2/version_test.go | 39 ++ 12 files changed, 988 insertions(+) create mode 100644 internal/eventstore/v2/aggregate.go create mode 100644 internal/eventstore/v2/event.go create mode 100644 internal/eventstore/v2/eventstore.go create mode 100644 internal/eventstore/v2/eventstore_memory.go create mode 100644 internal/eventstore/v2/eventstore_test.go create mode 100644 internal/eventstore/v2/filter.go create mode 100644 internal/eventstore/v2/filter_test.go create mode 100644 internal/eventstore/v2/read_model.go create mode 100644 internal/eventstore/v2/search_query.go create mode 100644 internal/eventstore/v2/search_query_test.go create mode 100644 internal/eventstore/v2/version.go create mode 100644 internal/eventstore/v2/version_test.go diff --git a/internal/eventstore/v2/aggregate.go b/internal/eventstore/v2/aggregate.go new file mode 100644 index 000000000..08c39b408 --- /dev/null +++ b/internal/eventstore/v2/aggregate.go @@ -0,0 +1,22 @@ +package eventstore + +//AggregateType is the object name +type AggregateType string + +//Aggregate represents an object +type Aggregate struct { + //ID id is the unique identifier of the aggregate + // the client must generate it by it's own + ID string + //Type describes the meaning of this aggregate + // it could an object like user + Type AggregateType + + //ResourceOwner is the organisation which owns this aggregate + // an aggregate can only be managed by one organisation + // use the ID of the org + ResourceOwner string + + //Events describe all the changes made on an aggregate + Events []*Event +} diff --git a/internal/eventstore/v2/event.go b/internal/eventstore/v2/event.go new file mode 100644 index 000000000..8e1cd270f --- /dev/null +++ b/internal/eventstore/v2/event.go @@ -0,0 +1,54 @@ +package eventstore + +import "time" + +//Event represents all information about a manipulation of an aggregate +type Event struct { + //ID is a generated uuid for this event + ID string + + //Sequence is the sequence of the event + Sequence uint64 + //PreviousSequence is the sequence of the previous sequence + // if it's 0 then it's the first event of this aggregate + PreviousSequence uint64 + + //CreationDate is the time the event is created + // it's used for human readability. + // Don't use it for event ordering, + // time drifts in different services could cause integrity problems + CreationDate time.Time + + //Type describes the cause of the event (e.g. user.added) + // it should always be in past-form + Type EventType + + //Data describe the changed fields (e.g. userName = "hodor") + // data must always a pointer to a struct, a struct or a byte array containing json bytes + Data interface{} + + //EditorService should be a unique identifier for the service which created the event + // it's meant for maintainability + EditorService string + //EditorUser should be a unique identifier for the user which created the event + // it's meant for maintainability. + // It's recommend to use the aggregate id of the user + EditorUser string + + //Version describes the definition of the aggregate at a certain point in time + // it's used in read models to reduce the events in the correct definition + Version Version + //AggregateID id is the unique identifier of the aggregate + // the client must generate it by it's own + AggregateID string + //AggregateType describes the meaning of the aggregate for this event + // it could an object like user + AggregateType AggregateType + //ResourceOwner is the organisation which owns this aggregate + // an aggregate can only be managed by one organisation + // use the ID of the org + ResourceOwner string +} + +//EventType is the description of the change +type EventType string diff --git a/internal/eventstore/v2/eventstore.go b/internal/eventstore/v2/eventstore.go new file mode 100644 index 000000000..c0bdb928d --- /dev/null +++ b/internal/eventstore/v2/eventstore.go @@ -0,0 +1,12 @@ +package eventstore + +import ( + "context" +) + +type Eventstore interface { + Health(ctx context.Context) error + PushAggregates(ctx context.Context, aggregates ...*Aggregate) error + FilterEvents(ctx context.Context, searchQuery *SearchQueryFactory) (events []*Event, err error) + LatestSequence(ctx context.Context, searchQuery *SearchQueryFactory) (uint64, error) +} diff --git a/internal/eventstore/v2/eventstore_memory.go b/internal/eventstore/v2/eventstore_memory.go new file mode 100644 index 000000000..1ca94aa23 --- /dev/null +++ b/internal/eventstore/v2/eventstore_memory.go @@ -0,0 +1,28 @@ +package eventstore + +import "context" + +type inMemoryEventstore struct { + events []*Event +} + +func (es *inMemoryEventstore) Health(ctx context.Context) error { + return nil +} + +func (es *inMemoryEventstore) PushAggregates(ctx context.Context, aggregates ...*Aggregate) error { + return nil +} + +func (es *inMemoryEventstore) FilterEvents(ctx context.Context, searchQuery *SearchQueryFactory) (events []*Event, err error) { + return nil, nil +} + +func (es *inMemoryEventstore) LatestSequence(ctx context.Context, searchQuery *SearchQueryFactory) (uint64, error) { + query, err := searchQuery.Build() + if err != nil { + return 0, err + } +query.Filters[0]. + return 0, nil +} diff --git a/internal/eventstore/v2/eventstore_test.go b/internal/eventstore/v2/eventstore_test.go new file mode 100644 index 000000000..8f4461acd --- /dev/null +++ b/internal/eventstore/v2/eventstore_test.go @@ -0,0 +1,24 @@ +package eventstore + +import "testing" + +func TestPushAggregates(t *testing.T) { + type res struct{} + type args struct{} + tests := []struct { + name string + args args + res *SearchQueryFactory + }{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // factory := NewSearchQueryFactory(tt.args.aggregateTypes...) + // for _, setter := range tt.args.setters { + // factory = setter(factory) + // } + // if !reflect.DeepEqual(factory, tt.res) { + // t.Errorf("NewSearchQueryFactory() = %v, want %v", factory, tt.res) + // } + }) + } +} diff --git a/internal/eventstore/v2/filter.go b/internal/eventstore/v2/filter.go new file mode 100644 index 000000000..a55cab74e --- /dev/null +++ b/internal/eventstore/v2/filter.go @@ -0,0 +1,67 @@ +package eventstore + +import ( + "github.com/caos/zitadel/internal/errors" +) + +type Filter struct { + field Field + value interface{} + operation Operation +} + +type Operation int32 + +const ( + Operation_Equals Operation = 1 + iota + Operation_Greater + Operation_Less + Operation_In +) + +type Field int32 + +const ( + Field_AggregateType Field = 1 + iota + Field_AggregateID + Field_LatestSequence + Field_ResourceOwner + Field_EditorService + Field_EditorUser + Field_EventType +) + +//NewFilter is used in tests. Use searchQuery.*Filter() instead +func NewFilter(field Field, value interface{}, operation Operation) *Filter { + return &Filter{ + field: field, + value: value, + operation: operation, + } +} + +func (f *Filter) Field() Field { + return f.field +} +func (f *Filter) Operation() Operation { + return f.operation +} +func (f *Filter) Value() interface{} { + return f.value +} + +func (f *Filter) Validate() error { + if f == nil { + return errors.ThrowPreconditionFailed(nil, "MODEL-z6KcG", "filter is nil") + } + if f.field <= 0 { + return errors.ThrowPreconditionFailed(nil, "MODEL-zw62U", "field not definded") + } + if f.value == nil { + return errors.ThrowPreconditionFailed(nil, "MODEL-GJ9ct", "no value definded") + } + if f.operation <= 0 { + return errors.ThrowPreconditionFailed(nil, "MODEL-RrQTy", "operation not definded") + } + return nil +} diff --git a/internal/eventstore/v2/filter_test.go b/internal/eventstore/v2/filter_test.go new file mode 100644 index 000000000..7b2fa0398 --- /dev/null +++ b/internal/eventstore/v2/filter_test.go @@ -0,0 +1,104 @@ +package eventstore + +import ( + "reflect" + "testing" +) + +func TestNewFilter(t *testing.T) { + type args struct { + field Field + value interface{} + operation Operation + } + tests := []struct { + name string + args args + want *Filter + }{ + { + name: "aggregateID equals", + args: args{ + field: Field_AggregateID, + value: "hodor", + operation: Operation_Equals, + }, + want: &Filter{field: Field_AggregateID, operation: Operation_Equals, value: "hodor"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewFilter(tt.args.field, tt.args.value, tt.args.operation); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFilter_Validate(t *testing.T) { + type fields struct { + field Field + value interface{} + operation Operation + isNil bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "correct filter", + fields: fields{ + field: Field_LatestSequence, + operation: Operation_Greater, + value: uint64(235), + }, + wantErr: false, + }, + { + name: "filter is nil", + fields: fields{isNil: true}, + wantErr: true, + }, + { + name: "no field error", + fields: fields{ + operation: Operation_Greater, + value: uint64(235), + }, + wantErr: true, + }, + { + name: "no value error", + fields: fields{ + field: Field_LatestSequence, + operation: Operation_Greater, + }, + wantErr: true, + }, + { + name: "no operation error", + fields: fields{ + field: Field_LatestSequence, + value: uint64(235), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var f *Filter + if !tt.fields.isNil { + f = &Filter{ + field: tt.fields.field, + value: tt.fields.value, + operation: tt.fields.operation, + } + } + if err := f.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Filter.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/eventstore/v2/read_model.go b/internal/eventstore/v2/read_model.go new file mode 100644 index 000000000..210dc8b24 --- /dev/null +++ b/internal/eventstore/v2/read_model.go @@ -0,0 +1,3 @@ +package eventstore + +type ReadModel struct{} diff --git a/internal/eventstore/v2/search_query.go b/internal/eventstore/v2/search_query.go new file mode 100644 index 000000000..2b10a7fb9 --- /dev/null +++ b/internal/eventstore/v2/search_query.go @@ -0,0 +1,152 @@ +package eventstore + +import ( + "github.com/caos/zitadel/internal/errors" +) + +type SearchQueryFactory struct { + columns Columns + limit uint64 + desc bool + aggregateTypes []AggregateType + aggregateIDs []string + eventSequence uint64 + eventTypes []EventType + resourceOwner string +} + +type searchQuery struct { + Columns Columns + Limit uint64 + Desc bool + Filters []*Filter +} + +type Columns int32 + +const ( + Columns_Event = iota + Columns_Max_Sequence + //insert new columns-types above this columnsCount because count is needed for validation + columnsCount +) + +func NewSearchQueryFactory(aggregateTypes ...AggregateType) *SearchQueryFactory { + return &SearchQueryFactory{ + aggregateTypes: aggregateTypes, + } +} + +func (factory *SearchQueryFactory) Columns(columns Columns) *SearchQueryFactory { + factory.columns = columns + return factory +} + +func (factory *SearchQueryFactory) Limit(limit uint64) *SearchQueryFactory { + factory.limit = limit + return factory +} + +func (factory *SearchQueryFactory) SequenceGreater(sequence uint64) *SearchQueryFactory { + factory.eventSequence = sequence + return factory +} + +func (factory *SearchQueryFactory) AggregateIDs(ids ...string) *SearchQueryFactory { + factory.aggregateIDs = ids + return factory +} + +func (factory *SearchQueryFactory) EventTypes(types ...EventType) *SearchQueryFactory { + factory.eventTypes = types + return factory +} + +func (factory *SearchQueryFactory) ResourceOwner(resourceOwner string) *SearchQueryFactory { + factory.resourceOwner = resourceOwner + return factory +} + +func (factory *SearchQueryFactory) OrderDesc() *SearchQueryFactory { + factory.desc = true + return factory +} + +func (factory *SearchQueryFactory) OrderAsc() *SearchQueryFactory { + factory.desc = false + return factory +} + +func (factory *SearchQueryFactory) Build() (*searchQuery, error) { + if factory == nil || + len(factory.aggregateTypes) < 1 || + (factory.columns < 0 || factory.columns >= columnsCount) { + return nil, errors.ThrowPreconditionFailed(nil, "MODEL-tGAD3", "factory invalid") + } + filters := []*Filter{ + factory.aggregateTypeFilter(), + } + + for _, f := range []func() *Filter{ + factory.aggregateIDFilter, + factory.eventSequenceFilter, + factory.eventTypeFilter, + factory.resourceOwnerFilter, + } { + if filter := f(); filter != nil { + filters = append(filters, filter) + } + } + + return &searchQuery{ + Columns: factory.columns, + Limit: factory.limit, + Desc: factory.desc, + Filters: filters, + }, nil +} + +func (factory *SearchQueryFactory) aggregateIDFilter() *Filter { + if len(factory.aggregateIDs) < 1 { + return nil + } + if len(factory.aggregateIDs) == 1 { + return NewFilter(Field_AggregateID, factory.aggregateIDs[0], Operation_Equals) + } + return NewFilter(Field_AggregateID, factory.aggregateIDs, Operation_In) +} + +func (factory *SearchQueryFactory) eventTypeFilter() *Filter { + if len(factory.eventTypes) < 1 { + return nil + } + if len(factory.eventTypes) == 1 { + return NewFilter(Field_EventType, factory.eventTypes[0], Operation_Equals) + } + return NewFilter(Field_EventType, factory.eventTypes, Operation_In) +} + +func (factory *SearchQueryFactory) aggregateTypeFilter() *Filter { + if len(factory.aggregateTypes) == 1 { + return NewFilter(Field_AggregateType, factory.aggregateTypes[0], Operation_Equals) + } + return NewFilter(Field_AggregateType, factory.aggregateTypes, Operation_In) +} + +func (factory *SearchQueryFactory) eventSequenceFilter() *Filter { + if factory.eventSequence == 0 { + return nil + } + sortOrder := Operation_Greater + if factory.desc { + sortOrder = Operation_Less + } + return NewFilter(Field_LatestSequence, factory.eventSequence, sortOrder) +} + +func (factory *SearchQueryFactory) resourceOwnerFilter() *Filter { + if factory.resourceOwner == "" { + return nil + } + return NewFilter(Field_ResourceOwner, factory.resourceOwner, Operation_Equals) +} diff --git a/internal/eventstore/v2/search_query_test.go b/internal/eventstore/v2/search_query_test.go new file mode 100644 index 000000000..ed0133b77 --- /dev/null +++ b/internal/eventstore/v2/search_query_test.go @@ -0,0 +1,465 @@ +package eventstore + +import ( + "reflect" + "testing" + + "github.com/caos/zitadel/internal/errors" +) + +func testSetColumns(columns Columns) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.Columns(columns) + return factory + } +} + +func testSetLimit(limit uint64) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.Limit(limit) + return factory + } +} + +func testSetSequence(sequence uint64) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.SequenceGreater(sequence) + return factory + } +} + +func testSetAggregateIDs(aggregateIDs ...string) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.AggregateIDs(aggregateIDs...) + return factory + } +} + +func testSetEventTypes(eventTypes ...EventType) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.EventTypes(eventTypes...) + return factory + } +} + +func testSetResourceOwner(resourceOwner string) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + factory = factory.ResourceOwner(resourceOwner) + return factory + } +} + +func testSetSortOrder(asc bool) func(factory *SearchQueryFactory) *SearchQueryFactory { + return func(factory *SearchQueryFactory) *SearchQueryFactory { + if asc { + factory = factory.OrderAsc() + } else { + factory = factory.OrderDesc() + } + return factory + } +} + +func TestSearchQueryFactorySetters(t *testing.T) { + type args struct { + aggregateTypes []AggregateType + setters []func(*SearchQueryFactory) *SearchQueryFactory + } + tests := []struct { + name string + args args + res *SearchQueryFactory + }{ + { + name: "New factory", + args: args{ + aggregateTypes: []AggregateType{"user", "org"}, + }, + res: &SearchQueryFactory{ + aggregateTypes: []AggregateType{"user", "org"}, + }, + }, + { + name: "set columns", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetColumns(Columns_Max_Sequence)}, + }, + res: &SearchQueryFactory{ + columns: Columns_Max_Sequence, + }, + }, + { + name: "set limit", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetLimit(100)}, + }, + res: &SearchQueryFactory{ + limit: 100, + }, + }, + { + name: "set sequence", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetSequence(90)}, + }, + res: &SearchQueryFactory{ + eventSequence: 90, + }, + }, + { + name: "set aggregateIDs", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetAggregateIDs("1235", "09824")}, + }, + res: &SearchQueryFactory{ + aggregateIDs: []string{"1235", "09824"}, + }, + }, + { + name: "set eventTypes", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetEventTypes("user.created", "user.updated")}, + }, + res: &SearchQueryFactory{ + eventTypes: []EventType{"user.created", "user.updated"}, + }, + }, + { + name: "set resource owner", + args: args{ + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetResourceOwner("hodor")}, + }, + res: &SearchQueryFactory{ + resourceOwner: "hodor", + }, + }, + { + name: "default search query", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{testSetAggregateIDs("1235", "024"), testSetSortOrder(false)}, + }, + res: &SearchQueryFactory{ + aggregateTypes: []AggregateType{"user"}, + aggregateIDs: []string{"1235", "024"}, + desc: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewSearchQueryFactory(tt.args.aggregateTypes...) + for _, setter := range tt.args.setters { + factory = setter(factory) + } + if !reflect.DeepEqual(factory, tt.res) { + t.Errorf("NewSearchQueryFactory() = %v, want %v", factory, tt.res) + } + }) + } +} + +func TestSearchQueryFactoryBuild(t *testing.T) { + type args struct { + aggregateTypes []AggregateType + setters []func(*SearchQueryFactory) *SearchQueryFactory + } + type res struct { + isErr func(err error) bool + query *searchQuery + } + tests := []struct { + name string + args args + res res + }{ + { + name: "no aggregate types", + args: args{ + aggregateTypes: []AggregateType{}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{}, + }, + res: res{ + isErr: errors.IsPreconditionFailed, + query: nil, + }, + }, + { + name: "invalid column (too low)", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetColumns(Columns(-1)), + }, + }, + res: res{ + isErr: errors.IsPreconditionFailed, + }, + }, + { + name: "invalid column (too high)", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetColumns(columnsCount), + }, + }, + res: res{ + isErr: errors.IsPreconditionFailed, + }, + }, + { + name: "filter aggregate type", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{}, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + }, + }, + }, + }, + { + name: "filter aggregate types", + args: args{ + aggregateTypes: []AggregateType{"user", "org"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{}, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, []AggregateType{"user", "org"}, Operation_In), + }, + }, + }, + }, + { + name: "filter aggregate type, limit, desc", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetLimit(5), + testSetSortOrder(false), + testSetSequence(100), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: true, + Limit: 5, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_LatestSequence, uint64(100), Operation_Less), + }, + }, + }, + }, + { + name: "filter aggregate type, limit, asc", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetLimit(5), + testSetSortOrder(true), + testSetSequence(100), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 5, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_LatestSequence, uint64(100), Operation_Greater), + }, + }, + }, + }, + { + name: "filter aggregate type, limit, desc, max event sequence cols", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetLimit(5), + testSetSortOrder(false), + testSetSequence(100), + testSetColumns(Columns_Max_Sequence), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: Columns_Max_Sequence, + Desc: true, + Limit: 5, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_LatestSequence, uint64(100), Operation_Less), + }, + }, + }, + }, + { + name: "filter aggregate type and aggregate id", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetAggregateIDs("1234"), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_AggregateID, "1234", Operation_Equals), + }, + }, + }, + }, + { + name: "filter aggregate type and aggregate ids", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetAggregateIDs("1234", "0815"), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_AggregateID, []string{"1234", "0815"}, Operation_In), + }, + }, + }, + }, + { + name: "filter aggregate type and sequence greater", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetSequence(8), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_LatestSequence, uint64(8), Operation_Greater), + }, + }, + }, + }, + { + name: "filter aggregate type and event type", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetEventTypes("user.created"), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_EventType, EventType("user.created"), Operation_Equals), + }, + }, + }, + }, + { + name: "filter aggregate type and event types", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetEventTypes("user.created", "user.changed"), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_EventType, []EventType{"user.created", "user.changed"}, Operation_In), + }, + }, + }, + }, + { + name: "filter aggregate type resource owner", + args: args{ + aggregateTypes: []AggregateType{"user"}, + setters: []func(*SearchQueryFactory) *SearchQueryFactory{ + testSetResourceOwner("hodor"), + }, + }, + res: res{ + isErr: nil, + query: &searchQuery{ + Columns: 0, + Desc: false, + Limit: 0, + Filters: []*Filter{ + NewFilter(Field_AggregateType, AggregateType("user"), Operation_Equals), + NewFilter(Field_ResourceOwner, "hodor", Operation_Equals), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewSearchQueryFactory(tt.args.aggregateTypes...) + for _, f := range tt.args.setters { + factory = f(factory) + } + query, err := factory.Build() + if tt.res.isErr != nil && !tt.res.isErr(err) { + t.Errorf("wrong error: %v", err) + return + } + if err != nil && tt.res.isErr == nil { + t.Errorf("no error expected: %v", err) + return + } + + if !reflect.DeepEqual(query, tt.res.query) { + t.Errorf("NewSearchQueryFactory() = %v, want %v", factory, tt.res) + } + }) + } +} diff --git a/internal/eventstore/v2/version.go b/internal/eventstore/v2/version.go new file mode 100644 index 000000000..27e0427f2 --- /dev/null +++ b/internal/eventstore/v2/version.go @@ -0,0 +1,18 @@ +package eventstore + +import ( + "regexp" + + "github.com/caos/zitadel/internal/errors" +) + +var versionRegexp = regexp.MustCompile(`^v[0-9]+(\.[0-9]+){0,2}$`) + +type Version string + +func (v Version) Validate() error { + if !versionRegexp.MatchString(string(v)) { + return errors.ThrowPreconditionFailed(nil, "MODEL-luDuS", "version is not semver") + } + return nil +} diff --git a/internal/eventstore/v2/version_test.go b/internal/eventstore/v2/version_test.go new file mode 100644 index 000000000..5bc92f9d1 --- /dev/null +++ b/internal/eventstore/v2/version_test.go @@ -0,0 +1,39 @@ +package eventstore + +import "testing" + +func TestVersion_Validate(t *testing.T) { + tests := []struct { + name string + v Version + wantErr bool + }{ + { + "correct version", + "v1.23.23", + false, + }, + { + "no v prefix", + "1.2.2", + true, + }, + { + "letters in version", + "v1.as.3", + true, + }, + { + "no version", + "", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.v.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Version.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}