diff --git a/internal/domain/human.go b/internal/domain/human.go index 429ffeea47..30700edca9 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -50,6 +50,10 @@ func (f Gender) Valid() bool { return f >= 0 && f < genderCount } +func (f Gender) Specified() bool { + return f > GenderUnspecified && f < genderCount +} + func (u *Human) IsValid() bool { return u.Username != "" && u.Profile != nil && u.Profile.IsValid() && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid()) } diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 6601480c16..23df19c598 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -56,6 +56,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"])) NewCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"])) NewFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["features"])) + NewUserProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["users"])) NewLoginNameProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["login_names"])) return nil diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go new file mode 100644 index 0000000000..791997528b --- /dev/null +++ b/internal/query/projection/user.go @@ -0,0 +1,705 @@ +package projection + +import ( + "context" + "database/sql" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "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/user" +) + +type UserProjection struct { + crdb.StatementHandler +} + +const ( + UserTable = "zitadel.projections.users" + UserHumanTable = UserTable + "_" + UserHumanSuffix + UserMachineTable = UserTable + "_" + UserMachineSuffix +) + +func NewUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) *UserProjection { + p := &UserProjection{} + config.ProjectionName = UserTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +const ( + UserIDCol = "id" + UserCreationDateCol = "creation_date" + UserChangeDateCol = "change_date" + UserResourceOwnerCol = "resource_owner" + UserStateCol = "state" + UserSequenceCol = "sequence" + UserUsernameCol = "username" +) + +const ( + UserHumanSuffix = "humans" + HumanUserIDCol = "user_id" + + // profile + HumanFistNameCol = "first_name" + HumanLastNameCol = "last_name" + HumanNickNameCol = "nick_name" + HumanDisplayNameCol = "display_name" + HumanPreferredLanguageCol = "preferred_language" + HumanGenderCol = "gender" + HumanAvaterURLCol = "avater_key" + + // email + HumanEmailCol = "email" + HumanIsEmailVerifiedCol = "is_email_verified" + + // phone + HumanPhoneCol = "phone" + HumanIsPhoneVerifiedCol = "is_phone_verified" +) + +const ( + UserMachineSuffix = "machines" + MachineUserIDCol = "user_id" + + MachineNameCol = "name" + MachineDescriptionCol = "description" +) + +func (p *UserProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: user.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: user.UserV1AddedType, + Reduce: p.reduceHumanAdded, + }, + { + Event: user.HumanAddedType, + Reduce: p.reduceHumanAdded, + }, + { + Event: user.UserV1RegisteredType, + Reduce: p.reduceHumanRegistered, + }, + { + Event: user.HumanRegisteredType, + Reduce: p.reduceHumanRegistered, + }, + { + Event: user.UserLockedType, + Reduce: p.reduceUserLocked, + }, + { + Event: user.UserUnlockedType, + Reduce: p.reduceUserUnlocked, + }, + { + Event: user.UserDeactivatedType, + Reduce: p.reduceUserDeactivated, + }, + { + Event: user.UserReactivatedType, + Reduce: p.reduceUserReactivated, + }, + { + Event: user.UserRemovedType, + Reduce: p.reduceUserRemoved, + }, + { + Event: user.UserUserNameChangedType, + Reduce: p.reduceUserNameChanged, + }, + { + Event: user.HumanProfileChangedType, + Reduce: p.reduceHumanProfileChanged, + }, + { + Event: user.UserV1ProfileChangedType, + Reduce: p.reduceHumanProfileChanged, + }, + { + Event: user.HumanPhoneChangedType, + Reduce: p.reduceHumanPhoneChanged, + }, + { + Event: user.UserV1PhoneChangedType, + Reduce: p.reduceHumanPhoneChanged, + }, + { + Event: user.HumanPhoneRemovedType, + Reduce: p.reduceHumanPhoneRemoved, + }, + { + Event: user.UserV1PhoneRemovedType, + Reduce: p.reduceHumanPhoneRemoved, + }, + { + Event: user.HumanPhoneVerifiedType, + Reduce: p.reduceHumanPhoneVerified, + }, + { + Event: user.UserV1PhoneVerifiedType, + Reduce: p.reduceHumanPhoneVerified, + }, + { + Event: user.HumanEmailChangedType, + Reduce: p.reduceHumanEmailChanged, + }, + { + Event: user.UserV1EmailChangedType, + Reduce: p.reduceHumanEmailChanged, + }, + { + Event: user.HumanEmailVerifiedType, + Reduce: p.reduceHumanEmailVerified, + }, + { + Event: user.UserV1EmailVerifiedType, + Reduce: p.reduceHumanEmailVerified, + }, + { + Event: user.HumanAvatarAddedType, + Reduce: p.reduceHumanAvatarAdded, + }, + { + Event: user.HumanAvatarRemovedType, + Reduce: p.reduceHumanAvatarRemoved, + }, + { + Event: user.MachineAddedEventType, + Reduce: p.reduceMachineAdded, + }, + { + Event: user.MachineChangedEventType, + Reduce: p.reduceMachineChanged, + }, + }, + }, + } +} + +func (p *UserProjection) reduceHumanAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanAddedEvent) + if !ok { + logging.LogWithFields("HANDL-Cw9BX", "seq", event.Sequence(), "expectedType", user.HumanAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-Ebynp", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(UserIDCol, e.Aggregate().ID), + handler.NewCol(UserCreationDateCol, e.CreationDate()), + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(UserStateCol, domain.UserStateInitial), + handler.NewCol(UserSequenceCol, e.Sequence()), + handler.NewCol(UserUsernameCol, e.UserName), + }, + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(HumanUserIDCol, e.Aggregate().ID), + handler.NewCol(HumanFistNameCol, e.FirstName), + handler.NewCol(HumanLastNameCol, e.LastName), + handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), + handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), + handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}), + handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), + handler.NewCol(HumanEmailCol, e.EmailAddress), + handler.NewCol(HumanPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanRegistered(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanRegisteredEvent) + if !ok { + logging.LogWithFields("HANDL-qoZyC", "seq", event.Sequence(), "expectedType", user.HumanRegisteredType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-xE53M", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(UserIDCol, e.Aggregate().ID), + handler.NewCol(UserCreationDateCol, e.CreationDate()), + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(UserStateCol, domain.UserStateInitial), + handler.NewCol(UserSequenceCol, e.Sequence()), + handler.NewCol(UserUsernameCol, e.UserName), + }, + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(HumanUserIDCol, e.Aggregate().ID), + handler.NewCol(HumanFistNameCol, e.FirstName), + handler.NewCol(HumanLastNameCol, e.LastName), + handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), + handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), + handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}), + handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), + handler.NewCol(HumanEmailCol, e.EmailAddress), + handler.NewCol(HumanPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceUserLocked(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UserLockedEvent) + if !ok { + logging.LogWithFields("HANDL-c6irw", "seq", event.Sequence(), "expectedType", user.UserLockedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-exyBF", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserStateCol, domain.UserStateLocked), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceUserUnlocked(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UserUnlockedEvent) + if !ok { + logging.LogWithFields("HANDL-eyHv5", "seq", event.Sequence(), "expectedType", user.UserUnlockedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-JIyRl", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserStateCol, domain.UserStateActive), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceUserDeactivated(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UserDeactivatedEvent) + if !ok { + logging.LogWithFields("HANDL-EqbaJ", "seq", event.Sequence(), "expectedType", user.UserDeactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-6BNjj", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserStateCol, domain.UserStateInactive), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceUserReactivated(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UserReactivatedEvent) + if !ok { + logging.LogWithFields("HANDL-kAaBr", "seq", event.Sequence(), "expectedType", user.UserReactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-IoF6j", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserStateCol, domain.UserStateActive), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceUserRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UserRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-n2JMe", "seq", event.Sequence(), "expectedType", user.UserRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-BQB2t", "reduce.wrong.event.type") + } + + return crdb.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceUserNameChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.UsernameChangedEvent) + if !ok { + logging.LogWithFields("HANDL-7J5xL", "seq", event.Sequence(), "expectedType", user.UserUserNameChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-QNKyV", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserUsernameCol, e.UserName), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *UserProjection) reduceHumanProfileChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanProfileChangedEvent) + if !ok { + logging.LogWithFields("HANDL-Dwfyn", "seq", event.Sequence(), "expectedType", user.HumanProfileChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-769v4", "reduce.wrong.event.type") + } + cols := make([]handler.Column, 0, 6) + cols = append(cols, + handler.NewCol(HumanFistNameCol, e.FirstName), + handler.NewCol(HumanLastNameCol, e.LastName), + ) + + if e.NickName != nil { + cols = append(cols, handler.NewCol(HumanNickNameCol, *e.NickName)) + } + + if e.DisplayName != nil { + cols = append(cols, handler.NewCol(HumanDisplayNameCol, *e.DisplayName)) + } + + if e.PreferredLanguage != nil { + cols = append(cols, handler.NewCol(HumanPreferredLanguageCol, e.PreferredLanguage.String())) + } + + if e.Gender != nil { + cols = append(cols, handler.NewCol(HumanGenderCol, *e.Gender)) + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + cols, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanPhoneChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanPhoneChangedEvent) + if !ok { + logging.LogWithFields("HANDL-pnRqf", "seq", event.Sequence(), "expectedType", user.HumanPhoneChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-xOGIA", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanPhoneCol, e.PhoneNumber), + handler.NewCol(HumanIsPhoneVerifiedCol, false), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanPhoneRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanPhoneRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-eMpOG", "seq", event.Sequence(), "expectedType", user.HumanPhoneRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-JI4S1", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanPhoneCol, nil), + handler.NewCol(HumanIsPhoneVerifiedCol, nil), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanPhoneVerified(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanPhoneVerifiedEvent) + if !ok { + logging.LogWithFields("HANDL-GhFOY", "seq", event.Sequence(), "expectedType", user.HumanPhoneVerifiedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-LBnqG", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanIsPhoneVerifiedCol, true), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanEmailChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanEmailChangedEvent) + if !ok { + logging.LogWithFields("HANDL-MDfHX", "seq", event.Sequence(), "expectedType", user.HumanEmailChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-KwiHa", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanEmailCol, e.EmailAddress), + handler.NewCol(HumanIsEmailVerifiedCol, false), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanEmailVerified(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanEmailVerifiedEvent) + if !ok { + logging.LogWithFields("HANDL-FdN0b", "seq", event.Sequence(), "expectedType", user.HumanEmailVerifiedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-JzcDq", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanIsEmailVerifiedCol, true), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanAvatarAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanAvatarAddedEvent) + if !ok { + logging.LogWithFields("HANDL-naQue", "seq", event.Sequence(), "expectedType", user.HumanAvatarAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-eDEdt", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanAvaterURLCol, e.StoreKey), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceHumanAvatarRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.HumanAvatarRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-c6zoV", "seq", event.Sequence(), "expectedType", user.HumanAvatarRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-KhETX", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(HumanAvaterURLCol, nil), + }, + []handler.Condition{ + handler.NewCond(HumanUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserHumanSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceMachineAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.MachineAddedEvent) + if !ok { + logging.LogWithFields("HANDL-8xr78", "seq", event.Sequence(), "expectedType", user.MachineAddedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-q7ier", "reduce.wrong.event.type") + } + + return crdb.NewMultiStatement( + e, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(UserIDCol, e.Aggregate().ID), + handler.NewCol(UserCreationDateCol, e.CreationDate()), + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(UserStateCol, domain.UserStateInitial), + handler.NewCol(UserSequenceCol, e.Sequence()), + handler.NewCol(UserUsernameCol, e.UserName), + }, + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(MachineUserIDCol, e.Aggregate().ID), + handler.NewCol(MachineNameCol, e.Name), + handler.NewCol(MachineDescriptionCol, &sql.NullString{String: e.Description, Valid: e.Description != ""}), + }, + crdb.WithTableSuffix(UserMachineSuffix), + ), + ), nil +} + +func (p *UserProjection) reduceMachineChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*user.MachineChangedEvent) + if !ok { + logging.LogWithFields("HANDL-uUFCy", "seq", event.Sequence(), "expectedType", user.MachineChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-qYHvj", "reduce.wrong.event.type") + } + + cols := make([]handler.Column, 0, 2) + if e.Name != nil { + cols = append(cols, handler.NewCol(MachineNameCol, *e.Name)) + } + if e.Description != nil { + cols = append(cols, handler.NewCol(MachineDescriptionCol, *e.Description)) + } + if len(cols) == 0 { + return crdb.NewNoOpStatement(e), nil + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + }, + ), + crdb.AddUpdateStatement( + cols, + []handler.Condition{ + handler.NewCond(MachineUserIDCol, e.Aggregate().ID), + }, + crdb.WithTableSuffix(UserMachineSuffix), + ), + ), nil +} diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go new file mode 100644 index 0000000000..466acab561 --- /dev/null +++ b/internal/query/projection/user_test.go @@ -0,0 +1,1319 @@ +package projection + +import ( + "database/sql" + "testing" + + "github.com/caos/zitadel/internal/domain" + "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/user" +) + +func TestUserProjection_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: "reduceHumanAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanAddedType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 1, + "email": "email@zitadel.ch", + "phone": "+41 00 000 00 00" + }`), + ), user.HumanAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{String: "nick-name", Valid: true}, + &sql.NullString{String: "display-name", Valid: true}, + &sql.NullString{String: "ch-DE", Valid: true}, + &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, + "email@zitadel.ch", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1Added", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1AddedType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 1, + "email": "email@zitadel.ch", + "phone": "+41 00 000 00 00" + }`), + ), user.HumanAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{String: "nick-name", Valid: true}, + &sql.NullString{String: "display-name", Valid: true}, + &sql.NullString{String: "ch-DE", Valid: true}, + &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, + "email@zitadel.ch", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanAdded NULLs", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanAddedType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "email": "email@zitadel.ch" + }`), + ), user.HumanAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{}, + &sql.NullString{}, + &sql.NullString{String: "und", Valid: false}, + &sql.NullInt16{}, + "email@zitadel.ch", + &sql.NullString{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanRegistered", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanRegisteredType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 1, + "email": "email@zitadel.ch", + "phone": "+41 00 000 00 00" + }`), + ), user.HumanRegisteredEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanRegistered, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{String: "nick-name", Valid: true}, + &sql.NullString{String: "display-name", Valid: true}, + &sql.NullString{String: "ch-DE", Valid: true}, + &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, + "email@zitadel.ch", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1Registered", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1RegisteredType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 1, + "email": "email@zitadel.ch", + "phone": "+41 00 000 00 00" + }`), + ), user.HumanRegisteredEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanRegistered, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{String: "nick-name", Valid: true}, + &sql.NullString{String: "display-name", Valid: true}, + &sql.NullString{String: "ch-DE", Valid: true}, + &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, + "email@zitadel.ch", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanRegistered NULLs", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanRegisteredType), + user.AggregateType, + []byte(`{ + "username": "user-name", + "firstName": "first-name", + "lastName": "last-name", + "email": "email@zitadel.ch" + }`), + ), user.HumanRegisteredEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanRegistered, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "user-name", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_humans (user_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "agg-id", + "first-name", + "last-name", + &sql.NullString{}, + &sql.NullString{}, + &sql.NullString{String: "und", Valid: false}, + &sql.NullInt16{}, + "email@zitadel.ch", + &sql.NullString{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceUserLocked", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserLockedType), + user.AggregateType, + []byte(`{}`), + ), user.UserLockedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserLocked, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserStateLocked, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserUnlocked", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserUnlockedType), + user.AggregateType, + []byte(`{}`), + ), user.UserUnlockedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserUnlocked, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserStateActive, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserDeactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserDeactivatedType), + user.AggregateType, + []byte(`{}`), + ), user.UserDeactivatedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserDeactivated, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserStateInactive, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserReactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserReactivatedType), + user.AggregateType, + []byte(`{}`), + ), user.UserReactivatedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserReactivated, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserStateActive, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserRemovedType), + user.AggregateType, + []byte(`{}`), + ), user.UserRemovedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.users WHERE (id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserUserNameChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserUserNameChangedType), + user.AggregateType, + []byte(`{ + "username": "username" + }`), + ), user.UsernameChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceUserNameChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + "username", + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanProfileChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanProfileChangedType), + user.AggregateType, + []byte(`{ + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 3 + }`), + ), user.HumanProfileChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanProfileChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7)", + expectedArgs: []interface{}{ + "first-name", + "last-name", + "nick-name", + "display-name", + "ch-DE", + domain.GenderDiverse, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1ProfileChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1ProfileChangedType), + user.AggregateType, + []byte(`{ + "firstName": "first-name", + "lastName": "last-name", + "nickName": "nick-name", + "displayName": "display-name", + "preferredLanguage": "ch-DE", + "gender": 3 + }`), + ), user.HumanProfileChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanProfileChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7)", + expectedArgs: []interface{}{ + "first-name", + "last-name", + "nick-name", + "display-name", + "ch-DE", + domain.GenderDiverse, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanPhoneChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanPhoneChangedType), + user.AggregateType, + []byte(`{ + "phone": "+41 00 000 00 00" + }`), + ), user.HumanPhoneChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + "+41 00 000 00 00", + false, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1PhoneChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1PhoneChangedType), + user.AggregateType, + []byte(`{ + "phone": "+41 00 000 00 00" + }`), + ), user.HumanPhoneChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + "+41 00 000 00 00", + false, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanPhoneRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanPhoneRemovedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanPhoneRemovedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + nil, + nil, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1PhoneRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1PhoneRemovedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanPhoneRemovedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + nil, + nil, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanPhoneVerified", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanPhoneVerifiedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanPhoneVerifiedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneVerified, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + true, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1PhoneVerified", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1PhoneVerifiedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanPhoneVerifiedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanPhoneVerified, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + true, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanEmailChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanEmailChangedType), + user.AggregateType, + []byte(`{ + "email": "email@zitadel.ch" + }`), + ), user.HumanEmailChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanEmailChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + "email@zitadel.ch", + false, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1EmailChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1EmailChangedType), + user.AggregateType, + []byte(`{ + "email": "email@zitadel.ch" + }`), + ), user.HumanEmailChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanEmailChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + "email@zitadel.ch", + false, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanEmailVerified", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanEmailVerifiedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanEmailVerifiedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanEmailVerified, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (is_email_verified) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + true, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserV1EmailVerified", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserV1EmailVerifiedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanEmailVerifiedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanEmailVerified, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (is_email_verified) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + true, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanAvatarAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanAvatarAddedType), + user.AggregateType, + []byte(`{ + "storeKey": "users/agg-id/avatar" + }`), + ), user.HumanAvatarAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanAvatarAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (avater_key) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + "users/agg-id/avatar", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceHumanAvatarRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.HumanAvatarRemovedType), + user.AggregateType, + []byte(`{}`), + ), user.HumanAvatarRemovedEventMapper), + }, + reduce: (&UserProjection{}).reduceHumanAvatarRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_humans SET (avater_key) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + nil, + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineAddedEvent no description", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineAddedEventType), + user.AggregateType, + []byte(`{ + "username": "username", + "name": "machine-name" + }`), + ), user.MachineAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "username", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_machines (user_id, name, description) VALUES ($1, $2, $3)", + expectedArgs: []interface{}{ + "agg-id", + "machine-name", + &sql.NullString{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineAddedEvent", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineAddedEventType), + user.AggregateType, + []byte(`{ + "username": "username", + "name": "machine-name", + "description": "description" + }`), + ), user.MachineAddedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineAdded, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.users (id, creation_date, change_date, resource_owner, state, sequence, username) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.UserStateInitial, + uint64(15), + "username", + }, + }, + { + expectedStmt: "INSERT INTO zitadel.projections.users_machines (user_id, name, description) VALUES ($1, $2, $3)", + expectedArgs: []interface{}{ + "agg-id", + "machine-name", + &sql.NullString{String: "description", Valid: true}, + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineChangedEvent", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineChangedEventType), + user.AggregateType, + []byte(`{ + "name": "machine-name", + "description": "description" + }`), + ), user.MachineChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_machines SET (name, description) = ($1, $2) WHERE (user_id = $3)", + expectedArgs: []interface{}{ + "machine-name", + "description", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineChangedEvent name", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineChangedEventType), + user.AggregateType, + []byte(`{ + "name": "machine-name" + }`), + ), user.MachineChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_machines SET (name) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + "machine-name", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineChangedEvent description", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineChangedEventType), + user.AggregateType, + []byte(`{ + "description": "description" + }`), + ), user.MachineChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.users_machines SET (description) = ($1) WHERE (user_id = $2)", + expectedArgs: []interface{}{ + "description", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineChangedEvent no values", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineChangedEventType), + user.AggregateType, + []byte(`{}`), + ), user.MachineChangedEventMapper), + }, + reduce: (&UserProjection{}).reduceMachineChanged, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserTable, + executer: &testExecuter{ + executions: []execution{}, + }, + }, + }, + } + 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/repository/user/human_phone.go b/internal/repository/user/human_phone.go index 9204cb8e4e..51a4757d36 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -3,11 +3,11 @@ package user import ( "context" "encoding/json" - "github.com/caos/zitadel/internal/eventstore" "time" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" ) @@ -81,7 +81,7 @@ func NewHumanPhoneRemovedEvent(ctx context.Context, aggregate *eventstore.Aggreg } func HumanPhoneRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { - return &HumanPhoneChangedEvent{ + return &HumanPhoneRemovedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), }, nil } diff --git a/migrations/cockroach/V1.95__user.sql b/migrations/cockroach/V1.95__user.sql new file mode 100644 index 0000000000..8258017210 --- /dev/null +++ b/migrations/cockroach/V1.95__user.sql @@ -0,0 +1,45 @@ +CREATE TABLE zitadel.projections.users( + id STRING + , creation_date TIMESTAMPTZ + , change_date TIMESTAMPTZ + , resource_owner STRING NOT NULL + , state INT2 + , sequence INT8 + + , username STRING + + , PRIMARY KEY (id) + , INDEX idx_username (username) +); + +CREATE TABLE zitadel.projections.users_machines( + user_id STRING REFERENCES zitadel.projections.users (id) ON DELETE CASCADE + + , name STRING NOT NULL + , description STRING + + , PRIMARY KEY (user_id) +); + +CREATE TABLE zitadel.projections.users_humans( + user_id STRING REFERENCES zitadel.projections.users (id) ON DELETE CASCADE + + --profile + , first_name STRING NOT NULL + , last_name STRING NOT NULL + , nick_name STRING + , display_name STRING + , preferred_language VARCHAR(10) + , gender INT2 + , avater_key STRING + + --email + , email STRING NOT NULL + , is_email_verified BOOLEAN NOT NULL DEFAULT false + + --phone + , phone STRING + , is_phone_verified BOOLEAN + + , PRIMARY KEY (user_id) +);