diff --git a/internal/query/projection/iam_member.go b/internal/query/projection/iam_member.go new file mode 100644 index 0000000000..d5064cd239 --- /dev/null +++ b/internal/query/projection/iam_member.go @@ -0,0 +1,97 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/iam" +) + +type IAMMemberProjection struct { + crdb.StatementHandler +} + +const ( + IAMMemberProjectionTable = "zitadel.projections.iam_members" +) + +func NewIAMMemberProjection(ctx context.Context, config crdb.StatementHandlerConfig) *IAMMemberProjection { + p := &IAMMemberProjection{} + config.ProjectionName = IAMMemberProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *IAMMemberProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: iam.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: iam.MemberAddedEventType, + Reduce: p.reduceAdded, + }, + { + Event: iam.MemberChangedEventType, + Reduce: p.reduceChanged, + }, + { + Event: iam.MemberCascadeRemovedEventType, + Reduce: p.reduceCascadeRemoved, + }, + { + Event: iam.MemberRemovedEventType, + Reduce: p.reduceRemoved, + }, + }, + }, + } +} + +type IAMMemberColumn string + +const ( + IAMMemberIAMIDCol = "iam_id" +) + +func (p *IAMMemberProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*iam.MemberAddedEvent) + if !ok { + logging.LogWithFields("HANDL-c8SBb", "seq", event.Sequence(), "expectedType", iam.MemberAddedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-pGNCu", "reduce.wrong.event.type") + } + return reduceMemberAdded(e.MemberAddedEvent, withMemberCol(IAMMemberIAMIDCol, e.Aggregate().ID)) +} + +func (p *IAMMemberProjection) reduceChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*iam.MemberChangedEvent) + if !ok { + logging.LogWithFields("HANDL-QsjwO", "seq", event.Sequence(), "expected", iam.MemberChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-5WQcZ", "reduce.wrong.event.type") + } + return reduceMemberChanged(e.MemberChangedEvent, withMemberCond(IAMMemberIAMIDCol, e.Aggregate().ID)) +} + +func (p *IAMMemberProjection) reduceCascadeRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*iam.MemberCascadeRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-mOncs", "seq", event.Sequence(), "expected", iam.MemberCascadeRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-Dmdf2", "reduce.wrong.event.type") + } + return reduceMemberCascadeRemoved(e.MemberCascadeRemovedEvent, withMemberCond(IAMMemberIAMIDCol, e.Aggregate().ID)) +} + +func (p *IAMMemberProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*iam.MemberRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-lW1Zv", "seq", event.Sequence(), "expected", iam.MemberRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-exVqy", "reduce.wrong.event.type") + } + return reduceMemberRemoved(e.MemberRemovedEvent, withMemberCond(IAMMemberIAMIDCol, e.Aggregate().ID)) +} diff --git a/internal/query/projection/iam_member_test.go b/internal/query/projection/iam_member_test.go new file mode 100644 index 0000000000..0413941620 --- /dev/null +++ b/internal/query/projection/iam_member_test.go @@ -0,0 +1,167 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestIAMMemberProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "iam.MemberAddedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.MemberAddedEventType), + iam.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role"] + }`), + ), iam.MemberAddedEventMapper), + }, + reduce: (&IAMMemberProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: iam.AggregateType, + sequence: 15, + previousSequence: 10, + projection: IAMMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.iam_members (user_id, roles, creation_date, change_date, sequence, resource_owner, iam_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "user-id", + []string{"role"}, + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "iam.MemberChangedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.MemberChangedEventType), + iam.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role", "changed"] + }`), + ), iam.MemberChangedEventMapper), + }, + reduce: (&IAMMemberProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: iam.AggregateType, + sequence: 15, + previousSequence: 10, + projection: IAMMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.iam_members SET (roles, change_date, sequence) = ($1, $2, $3) WHERE (user_id = $4) AND (iam_id = $5)", + expectedArgs: []interface{}{ + []string{"role", "changed"}, + anyArg{}, + uint64(15), + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "iam.MemberCascadeRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.MemberCascadeRemovedEventType), + iam.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), iam.MemberCascadeRemovedEventMapper), + }, + reduce: (&IAMMemberProjection{}).reduceCascadeRemoved, + want: wantReduce{ + aggregateType: iam.AggregateType, + sequence: 15, + previousSequence: 10, + projection: IAMMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.iam_members WHERE (user_id = $1) AND (iam_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "iam.MemberRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.MemberRemovedEventType), + iam.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), iam.MemberRemovedEventMapper), + }, + reduce: (&IAMMemberProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: iam.AggregateType, + sequence: 15, + previousSequence: 10, + projection: IAMMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.iam_members WHERE (user_id = $1) AND (iam_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/projection/member.go b/internal/query/projection/member.go new file mode 100644 index 0000000000..232282c578 --- /dev/null +++ b/internal/query/projection/member.go @@ -0,0 +1,98 @@ +package projection + +import ( + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/member" +) + +const ( + MemberUserIDCol = "user_id" + MemberRolesCol = "roles" + + MemberCreationDate = "creation_date" + MemberChangeDate = "change_date" + MemberSequence = "sequence" + MemberResourceOwner = "resource_owner" +) + +type reduceMemberConfig struct { + cols []handler.Column + conds []handler.Condition +} + +type reduceMemberOpt func(reduceMemberConfig) reduceMemberConfig + +func withMemberCol(col string, value interface{}) reduceMemberOpt { + return func(opt reduceMemberConfig) reduceMemberConfig { + opt.cols = append(opt.cols, handler.NewCol(col, value)) + return opt + } +} + +func withMemberCond(cond string, value interface{}) reduceMemberOpt { + return func(opt reduceMemberConfig) reduceMemberConfig { + opt.conds = append(opt.conds, handler.NewCond(cond, value)) + return opt + } +} + +func reduceMemberAdded(e member.MemberAddedEvent, opts ...reduceMemberOpt) (*handler.Statement, error) { + config := reduceMemberConfig{ + cols: []handler.Column{ + handler.NewCol(MemberUserIDCol, e.UserID), + handler.NewCol(MemberRolesCol, e.Roles), + handler.NewCol(MemberCreationDate, e.CreationDate()), + handler.NewCol(MemberChangeDate, e.CreationDate()), + handler.NewCol(MemberSequence, e.Sequence()), + handler.NewCol(MemberResourceOwner, e.Aggregate().ResourceOwner), + }} + + for _, opt := range opts { + config = opt(config) + } + + return crdb.NewCreateStatement(&e, config.cols), nil +} + +func reduceMemberChanged(e member.MemberChangedEvent, opts ...reduceMemberOpt) (*handler.Statement, error) { + config := reduceMemberConfig{ + cols: []handler.Column{ + handler.NewCol(MemberRolesCol, e.Roles), + handler.NewCol(MemberChangeDate, e.CreationDate()), + handler.NewCol(MemberSequence, e.Sequence()), + }, + conds: []handler.Condition{ + handler.NewCond(MemberUserIDCol, e.UserID), + }} + + for _, opt := range opts { + config = opt(config) + } + + return crdb.NewUpdateStatement(&e, config.cols, config.conds), nil +} + +func reduceMemberCascadeRemoved(e member.MemberCascadeRemovedEvent, opts ...reduceMemberOpt) (*handler.Statement, error) { + config := reduceMemberConfig{ + conds: []handler.Condition{ + handler.NewCond(MemberUserIDCol, e.UserID), + }} + + for _, opt := range opts { + config = opt(config) + } + return crdb.NewDeleteStatement(&e, config.conds), nil +} + +func reduceMemberRemoved(e member.MemberRemovedEvent, opts ...reduceMemberOpt) (*handler.Statement, error) { + config := reduceMemberConfig{ + conds: []handler.Condition{ + handler.NewCond(MemberUserIDCol, e.UserID), + }} + + for _, opt := range opts { + config = opt(config) + } + return crdb.NewDeleteStatement(&e, config.conds), nil +} diff --git a/internal/query/projection/org_member.go b/internal/query/projection/org_member.go new file mode 100644 index 0000000000..131a1dbbf4 --- /dev/null +++ b/internal/query/projection/org_member.go @@ -0,0 +1,97 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/org" +) + +type OrgMemberProjection struct { + crdb.StatementHandler +} + +const ( + OrgMemberProjectionTable = "zitadel.projections.org_members" +) + +func NewOrgMemberProjection(ctx context.Context, config crdb.StatementHandlerConfig) *OrgMemberProjection { + p := &OrgMemberProjection{} + config.ProjectionName = OrgMemberProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *OrgMemberProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: org.MemberAddedEventType, + Reduce: p.reduceAdded, + }, + { + Event: org.MemberChangedEventType, + Reduce: p.reduceChanged, + }, + { + Event: org.MemberCascadeRemovedEventType, + Reduce: p.reduceCascadeRemoved, + }, + { + Event: org.MemberRemovedEventType, + Reduce: p.reduceRemoved, + }, + }, + }, + } +} + +type OrgMemberColumn string + +const ( + OrgMemberOrgIDCol = "org_id" +) + +func (p *OrgMemberProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*org.MemberAddedEvent) + if !ok { + logging.LogWithFields("HANDL-BoKBr", "seq", event.Sequence(), "expectedType", org.MemberAddedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type") + } + return reduceMemberAdded(e.MemberAddedEvent, withMemberCol(OrgMemberOrgIDCol, e.Aggregate().ID)) +} + +func (p *OrgMemberProjection) reduceChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*org.MemberChangedEvent) + if !ok { + logging.LogWithFields("HANDL-bfqNl", "seq", event.Sequence(), "expected", org.MemberChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-Bg8oM", "reduce.wrong.event.type") + } + return reduceMemberChanged(e.MemberChangedEvent, withMemberCond(OrgMemberOrgIDCol, e.Aggregate().ID)) +} + +func (p *OrgMemberProjection) reduceCascadeRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*org.MemberCascadeRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-zgb6w", "seq", event.Sequence(), "expected", org.MemberCascadeRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-4twP2", "reduce.wrong.event.type") + } + return reduceMemberCascadeRemoved(e.MemberCascadeRemovedEvent, withMemberCond(OrgMemberOrgIDCol, e.Aggregate().ID)) +} + +func (p *OrgMemberProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*org.MemberRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-KPyxE", "seq", event.Sequence(), "expected", org.MemberRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-avatH", "reduce.wrong.event.type") + } + return reduceMemberRemoved(e.MemberRemovedEvent, withMemberCond(OrgMemberOrgIDCol, e.Aggregate().ID)) +} diff --git a/internal/query/projection/org_member_test.go b/internal/query/projection/org_member_test.go new file mode 100644 index 0000000000..65adfa6a9e --- /dev/null +++ b/internal/query/projection/org_member_test.go @@ -0,0 +1,167 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestOrgMemberProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "org.MemberAddedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.MemberAddedEventType), + org.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role"] + }`), + ), org.MemberAddedEventMapper), + }, + reduce: (&OrgMemberProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: org.AggregateType, + sequence: 15, + previousSequence: 10, + projection: OrgMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.org_members (user_id, roles, creation_date, change_date, sequence, resource_owner, org_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "user-id", + []string{"role"}, + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "org.MemberChangedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.MemberChangedEventType), + org.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role", "changed"] + }`), + ), org.MemberChangedEventMapper), + }, + reduce: (&OrgMemberProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: org.AggregateType, + sequence: 15, + previousSequence: 10, + projection: OrgMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.org_members SET (roles, change_date, sequence) = ($1, $2, $3) WHERE (user_id = $4) AND (org_id = $5)", + expectedArgs: []interface{}{ + []string{"role", "changed"}, + anyArg{}, + uint64(15), + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "org.MemberCascadeRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.MemberCascadeRemovedEventType), + org.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), org.MemberCascadeRemovedEventMapper), + }, + reduce: (&OrgMemberProjection{}).reduceCascadeRemoved, + want: wantReduce{ + aggregateType: org.AggregateType, + sequence: 15, + previousSequence: 10, + projection: OrgMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.org_members WHERE (user_id = $1) AND (org_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "org.MemberRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.MemberRemovedEventType), + org.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), org.MemberRemovedEventMapper), + }, + reduce: (&OrgMemberProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: org.AggregateType, + sequence: 15, + previousSequence: 10, + projection: OrgMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.org_members WHERE (user_id = $1) AND (org_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/projection/project_grant_member.go b/internal/query/projection/project_grant_member.go new file mode 100644 index 0000000000..eb5e4e9b68 --- /dev/null +++ b/internal/query/projection/project_grant_member.go @@ -0,0 +1,115 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/project" +) + +type ProjectGrantMemberProjection struct { + crdb.StatementHandler +} + +const ( + ProjectGrantMemberProjectionTable = "zitadel.projections.project_grant_members" +) + +func NewProjectGrantMemberProjection(ctx context.Context, config crdb.StatementHandlerConfig) *ProjectGrantMemberProjection { + p := &ProjectGrantMemberProjection{} + config.ProjectionName = ProjectGrantMemberProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *ProjectGrantMemberProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: project.GrantMemberAddedType, + Reduce: p.reduceAdded, + }, + { + Event: project.GrantMemberChangedType, + Reduce: p.reduceChanged, + }, + { + Event: project.GrantMemberCascadeRemovedType, + Reduce: p.reduceCascadeRemoved, + }, + { + Event: project.GrantMemberRemovedType, + Reduce: p.reduceRemoved, + }, + }, + }, + } +} + +type ProjectGrantMemberColumn string + +const ( + ProjectGrantMemberProjectIDCol = "project_id" + ProjectGrantMemberGrantIDCol = "grant_id" +) + +func (p *ProjectGrantMemberProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.GrantMemberAddedEvent) + if !ok { + logging.LogWithFields("HANDL-csr8B", "seq", event.Sequence(), "expectedType", project.GrantMemberAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-0EBQf", "reduce.wrong.event.type") + } + return reduceMemberAdded( + *member.NewMemberAddedEvent(&e.BaseEvent, e.UserID, e.Roles...), + withMemberCol(ProjectGrantMemberProjectIDCol, e.Aggregate().ID), + withMemberCol(ProjectGrantMemberGrantIDCol, e.GrantID), + ) +} + +func (p *ProjectGrantMemberProjection) reduceChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.GrantMemberChangedEvent) + if !ok { + logging.LogWithFields("HANDL-ZubbI", "seq", event.Sequence(), "expectedType", project.GrantMemberChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-YX5Tk", "reduce.wrong.event.type") + } + return reduceMemberChanged( + *member.NewMemberChangedEvent(&e.BaseEvent, e.UserID, e.Roles...), + withMemberCond(ProjectGrantMemberProjectIDCol, e.Aggregate().ID), + withMemberCond(ProjectGrantMemberGrantIDCol, e.GrantID), + ) +} + +func (p *ProjectGrantMemberProjection) reduceCascadeRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.GrantMemberCascadeRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-azx7K", "seq", event.Sequence(), "expectedType", project.GrantMemberCascadeRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-adnHG", "reduce.wrong.event.type") + } + return reduceMemberCascadeRemoved( + *member.NewCascadeRemovedEvent(&e.BaseEvent, e.UserID), + withMemberCond(ProjectGrantMemberProjectIDCol, e.Aggregate().ID), + withMemberCond(ProjectGrantMemberGrantIDCol, e.GrantID), + ) +} + +func (p *ProjectGrantMemberProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.GrantMemberRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-6Z4dH", "seq", event.Sequence(), "expectedType", project.GrantMemberRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-MGNnA", "reduce.wrong.event.type") + } + return reduceMemberRemoved( + *member.NewRemovedEvent(&e.BaseEvent, e.UserID), + withMemberCond(ProjectGrantMemberProjectIDCol, e.Aggregate().ID), + withMemberCond(ProjectGrantMemberGrantIDCol, e.GrantID), + ) +} diff --git a/internal/query/projection/project_grant_member_test.go b/internal/query/projection/project_grant_member_test.go new file mode 100644 index 0000000000..73681d02a9 --- /dev/null +++ b/internal/query/projection/project_grant_member_test.go @@ -0,0 +1,175 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" +) + +func TestProjectGrantMemberProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "project.GrantMemberAddedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.GrantMemberAddedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role"], + "grantId": "grant-id" + }`), + ), project.GrantMemberAddedEventMapper), + }, + reduce: (&ProjectGrantMemberProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectGrantMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.project_grant_members (user_id, roles, creation_date, change_date, sequence, resource_owner, project_id, grant_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedArgs: []interface{}{ + "user-id", + []string{"role"}, + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "agg-id", + "grant-id", + }, + }, + }, + }, + }, + }, + { + name: "project.GrantMemberChangedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.GrantMemberChangedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role", "changed"], + "grantId": "grant-id" + }`), + ), project.GrantMemberChangedEventMapper), + }, + reduce: (&ProjectGrantMemberProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectGrantMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.project_grant_members SET (roles, change_date, sequence) = ($1, $2, $3) WHERE (user_id = $4) AND (project_id = $5) AND (grant_id = $6)", + expectedArgs: []interface{}{ + []string{"role", "changed"}, + anyArg{}, + uint64(15), + "user-id", + "agg-id", + "grant-id", + }, + }, + }, + }, + }, + }, + { + name: "project.GrantMemberCascadeRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.GrantMemberCascadeRemovedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "grantId": "grant-id" + }`), + ), project.GrantMemberCascadeRemovedEventMapper), + }, + reduce: (&ProjectGrantMemberProjection{}).reduceCascadeRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectGrantMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.project_grant_members WHERE (user_id = $1) AND (project_id = $2) AND (grant_id = $3)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + "grant-id", + }, + }, + }, + }, + }, + }, + { + name: "project.GrantMemberRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.GrantMemberRemovedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "grantId": "grant-id" + }`), + ), project.GrantMemberRemovedEventMapper), + }, + reduce: (&ProjectGrantMemberProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectGrantMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.project_grant_members WHERE (user_id = $1) AND (project_id = $2) AND (grant_id = $3)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + "grant-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/projection/project_member.go b/internal/query/projection/project_member.go new file mode 100644 index 0000000000..62b303fbf6 --- /dev/null +++ b/internal/query/projection/project_member.go @@ -0,0 +1,110 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/project" +) + +type ProjectMemberProjection struct { + crdb.StatementHandler +} + +const ( + ProjectMemberProjectionTable = "zitadel.projections.project_members" +) + +func NewProjectMemberProjection(ctx context.Context, config crdb.StatementHandlerConfig) *ProjectMemberProjection { + p := &ProjectMemberProjection{} + config.ProjectionName = ProjectMemberProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *ProjectMemberProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: project.MemberAddedType, + Reduce: p.reduceAdded, + }, + { + Event: project.MemberChangedType, + Reduce: p.reduceChanged, + }, + { + Event: project.MemberCascadeRemovedType, + Reduce: p.reduceCascadeRemoved, + }, + { + Event: project.MemberRemovedType, + Reduce: p.reduceRemoved, + }, + }, + }, + } +} + +type ProjectMemberColumn string + +const ( + ProjectMemberProjectIDCol = "project_id" +) + +func (p *ProjectMemberProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.MemberAddedEvent) + if !ok { + logging.LogWithFields("HANDL-3FRys", "seq", event.Sequence(), "expectedType", project.MemberAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-bgx5Q", "reduce.wrong.event.type") + } + return reduceMemberAdded( + *member.NewMemberAddedEvent(&e.BaseEvent, e.UserID, e.Roles...), + withMemberCol(ProjectMemberProjectIDCol, e.Aggregate().ID), + ) +} + +func (p *ProjectMemberProjection) reduceChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.MemberChangedEvent) + if !ok { + logging.LogWithFields("HANDL-9hgMR", "seq", event.Sequence(), "expectedType", project.MemberChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-90WJ1", "reduce.wrong.event.type") + } + return reduceMemberChanged( + *member.NewMemberChangedEvent(&e.BaseEvent, e.UserID, e.Roles...), + withMemberCond(ProjectMemberProjectIDCol, e.Aggregate().ID), + ) +} + +func (p *ProjectMemberProjection) reduceCascadeRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.MemberCascadeRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-2kyYa", "seq", event.Sequence(), "expectedType", project.MemberCascadeRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-aGd43", "reduce.wrong.event.type") + } + return reduceMemberCascadeRemoved( + *member.NewCascadeRemovedEvent(&e.BaseEvent, e.UserID), + withMemberCond(ProjectMemberProjectIDCol, e.Aggregate().ID), + ) +} + +func (p *ProjectMemberProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.MemberRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-X0yvM", "seq", event.Sequence(), "expectedType", project.MemberRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-eJZPh", "reduce.wrong.event.type") + } + return reduceMemberRemoved( + *member.NewRemovedEvent(&e.BaseEvent, e.UserID), + withMemberCond(ProjectMemberProjectIDCol, e.Aggregate().ID), + ) +} diff --git a/internal/query/projection/project_member_test.go b/internal/query/projection/project_member_test.go new file mode 100644 index 0000000000..5c64b2f70e --- /dev/null +++ b/internal/query/projection/project_member_test.go @@ -0,0 +1,167 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" +) + +func TestProjectMemberProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "project.MemberAddedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.MemberAddedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role"] + }`), + ), project.MemberAddedEventMapper), + }, + reduce: (&ProjectMemberProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.project_members (user_id, roles, creation_date, change_date, sequence, resource_owner, project_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "user-id", + []string{"role"}, + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "project.MemberChangedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.MemberChangedType), + project.AggregateType, + []byte(`{ + "userId": "user-id", + "roles": ["role", "changed"] + }`), + ), project.MemberChangedEventMapper), + }, + reduce: (&ProjectMemberProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.project_members SET (roles, change_date, sequence) = ($1, $2, $3) WHERE (user_id = $4) AND (project_id = $5)", + expectedArgs: []interface{}{ + []string{"role", "changed"}, + anyArg{}, + uint64(15), + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "project.MemberCascadeRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.MemberCascadeRemovedType), + project.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), project.MemberCascadeRemovedEventMapper), + }, + reduce: (&ProjectMemberProjection{}).reduceCascadeRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.project_members WHERE (user_id = $1) AND (project_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "project.MemberRemovedType", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.MemberRemovedType), + project.AggregateType, + []byte(`{ + "userId": "user-id" + }`), + ), project.MemberRemovedEventMapper), + }, + reduce: (&ProjectMemberProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: ProjectMemberProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.project_members WHERE (user_id = $1) AND (project_id = $2)", + expectedArgs: []interface{}{ + "user-id", + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/migrations/cockroach/V1.96__members.sql b/migrations/cockroach/V1.96__members.sql new file mode 100644 index 0000000000..1d1e51c7b7 --- /dev/null +++ b/migrations/cockroach/V1.96__members.sql @@ -0,0 +1,56 @@ +CREATE TABLE zitadel.projections.org_members ( + org_id STRING NOT NULL + , user_id STRING NOT NULL + , roles STRING[] + + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , sequence INT8 NOT NULL + , resource_owner STRING NOT NULL + + , PRIMARY KEY (org_id, user_id) + , INDEX idx_user (user_id) +); + +CREATE TABLE zitadel.projections.iam_members ( + iam_id STRING NOT NULL + , user_id STRING NOT NULL + , roles STRING[] + + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , sequence INT8 NOT NULL + , resource_owner STRING NOT NULL + + , PRIMARY KEY (iam_id, user_id) + , INDEX idx_user (user_id) +); + +CREATE TABLE zitadel.projections.project_members ( + project_id STRING NOT NULL + , user_id STRING NOT NULL + , roles STRING[] + + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , sequence INT8 NOT NULL + , resource_owner STRING NOT NULL + + , PRIMARY KEY (project_id, user_id) + , INDEX idx_user (user_id) +); + +CREATE TABLE zitadel.projections.project_grant_members ( + project_id STRING NOT NULL + , user_id STRING NOT NULL + , grant_id STRING + , roles STRING[] + + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , sequence INT8 NOT NULL + , resource_owner STRING NOT NULL + + , PRIMARY KEY (project_id, grant_id, user_id) + , INDEX idx_user (user_id) +);