feat: feature query (#2578)

* feat: features projection

* feat: tests

* fix: update version
This commit is contained in:
Fabi
2021-10-29 16:33:00 +02:00
committed by GitHub
parent a1f4a06d27
commit 4c1be86ce2
5 changed files with 669 additions and 0 deletions

230
internal/query/features.go Normal file
View File

@@ -0,0 +1,230 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query/projection"
)
type Feature struct {
AggregateID string
CreationDate time.Time
ChangeDate time.Time
Sequence uint64
IsDefault bool
TierName string
TierDescription string
State domain.FeaturesState
StateDescription string
AuditLogRetention time.Duration
LoginPolicyFactors bool
LoginPolicyIDP bool
LoginPolicyPasswordless bool
LoginPolicyRegistration bool
LoginPolicyUsernameLogin bool
LoginPolicyPasswordReset bool
PasswordComplexityPolicy bool
LabelPolicyPrivateLabel bool
LabelPolicyWatermark bool
CustomDomain bool
PrivacyPolicy bool
MetadataUser bool
CustomTextMessage bool
CustomTextLogin bool
LockoutPolicy bool
Actions bool
}
var (
feautureTable = table{
name: projection.FeatureTable,
}
FeatureColumnAggregateID = Column{
name: projection.FeatureAggregateIDCol,
}
FeatureColumnCreationDate = Column{
name: projection.FeatureCreationDateCol,
}
FeatureColumnChangeDate = Column{
name: projection.FeatureChangeDateCol,
}
FeatureColumnSequence = Column{
name: projection.FeatureSequenceCol,
}
FeatureColumnIsDefault = Column{
name: projection.FeatureIsDefaultCol,
}
FeatureTierName = Column{
name: projection.FeatureTierNameCol,
}
FeatureTierDescription = Column{
name: projection.FeatureTierDescriptionCol,
}
FeatureState = Column{
name: projection.FeatureStateCol,
}
FeatureStateDescription = Column{
name: projection.FeatureStateDescriptionCol,
}
FeatureAuditLogRetention = Column{
name: projection.FeatureAuditLogRetentionCol,
}
FeatureLoginPolicyFactors = Column{
name: projection.FeatureLoginPolicyFactorsCol,
}
FeatureLoginPolicyIDP = Column{
name: projection.FeatureLoginPolicyIDPCol,
}
FeatureLoginPolicyPasswordless = Column{
name: projection.FeatureLoginPolicyPasswordlessCol,
}
FeatureLoginPolicyRegistration = Column{
name: projection.FeatureLoginPolicyRegistrationCol,
}
FeatureLoginPolicyUsernameLogin = Column{
name: projection.FeatureLoginPolicyUsernameLoginCol,
}
FeatureLoginPolicyPasswordReset = Column{
name: projection.FeatureLoginPolicyPasswordResetCol,
}
FeaturePasswordComplexityPolicy = Column{
name: projection.FeaturePasswordComplexityPolicyCol,
}
FeatureLabelPolicyPrivateLabel = Column{
name: projection.FeatureLabelPolicyPrivateLabelCol,
}
FeatureLabelPolicyWatermark = Column{
name: projection.FeatureLabelPolicyWatermarkCol,
}
FeatureCustomDomain = Column{
name: projection.FeatureCustomDomainCol,
}
FeaturePrivacyPolicy = Column{
name: projection.FeaturePrivacyPolicyCol,
}
FeatureMetadataUser = Column{
name: projection.FeatureMetadataUserCol,
}
FeatureCustomTextMessage = Column{
name: projection.FeatureCustomTextMessageCol,
}
FeatureCustomTextLogin = Column{
name: projection.FeatureCustomTextLoginCol,
}
FeatureLockoutPolicy = Column{
name: projection.FeatureLockoutPolicyCol,
}
FeatureActions = Column{
name: projection.FeatureActionsCol,
}
)
func (q *Queries) FeatureByID(ctx context.Context, orgID string) (*Feature, error) {
query, scan := prepareFeatureQuery()
stmt, args, err := query.Where(
sq.Or{
sq.Eq{
FeatureColumnAggregateID.identifier(): orgID,
},
sq.Eq{
FeatureColumnAggregateID.identifier(): domain.IAMID,
},
}).
OrderBy(FeatureColumnIsDefault.identifier()).
Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-P9gwg", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, stmt, args...)
return scan(row)
}
func (q *Queries) DefaultFeature(ctx context.Context) (*Feature, error) {
query, scan := prepareFeatureQuery()
stmt, args, err := query.Where(sq.Eq{
FeatureColumnAggregateID.identifier(): domain.IAMID,
}).OrderBy(FeatureColumnIsDefault.identifier()).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-1Ndlg", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, stmt, args...)
return scan(row)
}
func prepareFeatureQuery() (sq.SelectBuilder, func(*sql.Row) (*Feature, error)) {
return sq.Select(
FeatureColumnAggregateID.identifier(),
FeatureColumnCreationDate.identifier(),
FeatureColumnChangeDate.identifier(),
FeatureColumnSequence.identifier(),
FeatureColumnIsDefault.identifier(),
FeatureTierName.identifier(),
FeatureTierDescription.identifier(),
FeatureState.identifier(),
FeatureStateDescription.identifier(),
FeatureAuditLogRetention.identifier(),
FeatureLoginPolicyFactors.identifier(),
FeatureLoginPolicyIDP.identifier(),
FeatureLoginPolicyPasswordless.identifier(),
FeatureLoginPolicyRegistration.identifier(),
FeatureLoginPolicyUsernameLogin.identifier(),
FeatureLoginPolicyPasswordReset.identifier(),
FeaturePasswordComplexityPolicy.identifier(),
FeatureLabelPolicyPrivateLabel.identifier(),
FeatureLabelPolicyWatermark.identifier(),
FeatureCustomDomain.identifier(),
FeaturePrivacyPolicy.identifier(),
FeatureMetadataUser.identifier(),
FeatureCustomTextMessage.identifier(),
FeatureCustomTextLogin.identifier(),
FeatureLockoutPolicy.identifier(),
FeatureActions.identifier(),
).From(loginPolicyTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Feature, error) {
p := new(Feature)
err := row.Scan(
&p.AggregateID,
&p.CreationDate,
&p.ChangeDate,
&p.Sequence,
&p.IsDefault,
&p.TierName,
&p.TierDescription,
&p.State,
&p.StateDescription,
&p.AuditLogRetention,
&p.LoginPolicyFactors,
&p.LoginPolicyIDP,
&p.LoginPolicyPasswordless,
&p.LoginPolicyRegistration,
&p.LoginPolicyRegistration,
&p.LoginPolicyUsernameLogin,
&p.LoginPolicyPasswordReset,
&p.PasswordComplexityPolicy,
&p.LabelPolicyPrivateLabel,
&p.LabelPolicyWatermark,
&p.CustomDomain,
&p.PrivacyPolicy,
&p.MetadataUser,
&p.CustomTextMessage,
&p.CustomTextLogin,
&p.LockoutPolicy,
&p.Actions,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-M9fse", "Errors.Feature.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-3o9gd", "Errors.Internal")
}
return p, nil
}
}

View File

@@ -0,0 +1,191 @@
package projection
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/repository/features"
"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"
"github.com/caos/zitadel/internal/repository/org"
)
type FeatureProjection struct {
crdb.StatementHandler
}
const (
FeatureTable = "zitadel.projections.features"
)
func NewFeatureProjection(ctx context.Context, config crdb.StatementHandlerConfig) *FeatureProjection {
p := &FeatureProjection{}
config.ProjectionName = FeatureTable
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *FeatureProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.FeaturesSetEventType,
Reduce: p.reduceFeatureSet,
},
{
Event: org.FeaturesRemovedEventType,
Reduce: p.reduceFeatureRemoved,
},
},
},
{
Aggregate: iam.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: iam.FeaturesSetEventType,
Reduce: p.reduceFeatureSet,
},
},
},
}
}
const (
FeatureAggregateIDCol = "aggregate_id"
FeatureCreationDateCol = "creation_date"
FeatureChangeDateCol = "change_date"
FeatureSequenceCol = "sequence"
FeatureIsDefaultCol = "is_default"
FeatureTierNameCol = "tier_name"
FeatureTierDescriptionCol = "tier_description"
FeatureStateCol = "state"
FeatureStateDescriptionCol = "state_description"
FeatureAuditLogRetentionCol = "audit_log_retention"
FeatureLoginPolicyFactorsCol = "login_policy_factors"
FeatureLoginPolicyIDPCol = "login_policy_idp"
FeatureLoginPolicyPasswordlessCol = "login_policy_passwordless"
FeatureLoginPolicyRegistrationCol = "login_policy_registration"
FeatureLoginPolicyUsernameLoginCol = "login_policy_username_login"
FeatureLoginPolicyPasswordResetCol = "login_policy_password_reset"
FeaturePasswordComplexityPolicyCol = "password_complexity_policy"
FeatureLabelPolicyPrivateLabelCol = "label_policy_private_label"
FeatureLabelPolicyWatermarkCol = "label_policy_watermark"
FeatureCustomDomainCol = "custom_domain"
FeaturePrivacyPolicyCol = "privacy_policy"
FeatureMetadataUserCol = "metadata_user"
FeatureCustomTextMessageCol = "custom_text_message"
FeatureCustomTextLoginCol = "custom_text_login"
FeatureLockoutPolicyCol = "lockout_policy"
FeatureActionsCol = "actions"
)
func (p *FeatureProjection) reduceFeatureSet(event eventstore.EventReader) (*handler.Statement, error) {
var featureEvent features.FeaturesSetEvent
var isDefault bool
switch e := event.(type) {
case *iam.FeaturesSetEvent:
featureEvent = e.FeaturesSetEvent
isDefault = true
case *org.FeaturesSetEvent:
featureEvent = e.FeaturesSetEvent
isDefault = false
default:
logging.LogWithFields("HANDL-M9ets", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.FeaturesSetEventType, iam.FeaturesSetEventType}).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-K0erf", "reduce.wrong.event.type")
}
cols := []handler.Column{
handler.NewCol(FeatureAggregateIDCol, featureEvent.Aggregate().ID),
handler.NewCol(FeatureCreationDateCol, featureEvent.CreationDate()),
handler.NewCol(FeatureChangeDateCol, featureEvent.CreationDate()),
handler.NewCol(FeatureSequenceCol, featureEvent.Sequence()),
handler.NewCol(FeatureIsDefaultCol, isDefault),
}
if featureEvent.TierName != nil {
cols = append(cols, handler.NewCol(FeatureTierNameCol, *featureEvent.TierName))
}
if featureEvent.TierDescription != nil {
cols = append(cols, handler.NewCol(FeatureTierDescriptionCol, *featureEvent.TierDescription))
}
if featureEvent.State != nil {
cols = append(cols, handler.NewCol(FeatureStateCol, *featureEvent.State))
}
if featureEvent.StateDescription != nil {
cols = append(cols, handler.NewCol(FeatureStateDescriptionCol, *featureEvent.StateDescription))
}
if featureEvent.AuditLogRetention != nil {
cols = append(cols, handler.NewCol(FeatureAuditLogRetentionCol, *featureEvent.AuditLogRetention))
}
if featureEvent.LoginPolicyFactors != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyFactorsCol, *featureEvent.LoginPolicyFactors))
}
if featureEvent.LoginPolicyIDP != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyIDPCol, *featureEvent.LoginPolicyIDP))
}
if featureEvent.LoginPolicyPasswordless != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyPasswordlessCol, *featureEvent.LoginPolicyPasswordless))
}
if featureEvent.LoginPolicyRegistration != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyRegistrationCol, *featureEvent.LoginPolicyRegistration))
}
if featureEvent.LoginPolicyUsernameLogin != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyUsernameLoginCol, *featureEvent.LoginPolicyUsernameLogin))
}
if featureEvent.LoginPolicyPasswordReset != nil {
cols = append(cols, handler.NewCol(FeatureLoginPolicyPasswordResetCol, *featureEvent.LoginPolicyPasswordReset))
}
if featureEvent.PasswordComplexityPolicy != nil {
cols = append(cols, handler.NewCol(FeaturePasswordComplexityPolicyCol, *featureEvent.PasswordComplexityPolicy))
}
if featureEvent.LabelPolicyPrivateLabel != nil {
cols = append(cols, handler.NewCol(FeatureLabelPolicyPrivateLabelCol, *featureEvent.LabelPolicyPrivateLabel))
}
if featureEvent.LabelPolicyWatermark != nil {
cols = append(cols, handler.NewCol(FeatureLabelPolicyWatermarkCol, *featureEvent.LabelPolicyWatermark))
}
if featureEvent.CustomDomain != nil {
cols = append(cols, handler.NewCol(FeatureCustomDomainCol, *featureEvent.CustomDomain))
}
if featureEvent.PrivacyPolicy != nil {
cols = append(cols, handler.NewCol(FeaturePrivacyPolicyCol, *featureEvent.PrivacyPolicy))
}
if featureEvent.MetadataUser != nil {
cols = append(cols, handler.NewCol(FeatureMetadataUserCol, *featureEvent.MetadataUser))
}
if featureEvent.CustomTextMessage != nil {
cols = append(cols, handler.NewCol(FeatureCustomTextMessageCol, *featureEvent.CustomTextMessage))
}
if featureEvent.CustomTextLogin != nil {
cols = append(cols, handler.NewCol(FeatureCustomTextLoginCol, *featureEvent.CustomTextLogin))
}
if featureEvent.LockoutPolicy != nil {
cols = append(cols, handler.NewCol(FeatureLockoutPolicyCol, *featureEvent.LockoutPolicy))
}
if featureEvent.Actions != nil {
cols = append(cols, handler.NewCol(FeatureActionsCol, *featureEvent.Actions))
}
return crdb.NewUpsertStatement(
&featureEvent,
cols), nil
}
func (p *FeatureProjection) reduceFeatureRemoved(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.FeaturesRemovedEvent)
if !ok {
logging.LogWithFields("HANDL-fN903", "seq", event.Sequence(), "expectedType", org.FeaturesRemovedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-0p4rf", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(FeatureAggregateIDCol, e.Aggregate().ID),
},
), nil
}

View File

@@ -0,0 +1,215 @@
package projection
import (
"testing"
"time"
"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/iam"
"github.com/caos/zitadel/internal/repository/org"
)
func TestFeatureProjection_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.reduceFeatureSet",
args: args{
event: getEvent(testEvent(
repository.EventType(org.FeaturesSetEventType),
org.AggregateType,
[]byte(`{
"tierName": "TierName",
"tierDescription": "TierDescription",
"state": 1,
"stateDescription": "StateDescription",
"auditLogRetention": 1,
"loginPolicyFactors": true,
"loginPolicyIDP": true,
"loginPolicyPasswordless": true,
"loginPolicyRegistration": true,
"loginPolicyUsernameLogin": true,
"loginPolicyPasswordReset": true,
"passwordComplexityPolicy": true,
"labelPolicyPrivateLabel": true,
"labelPolicyWatermark": true,
"customDomain": true,
"privacyPolicy": true,
"metadataUser": true,
"customTextMessage": true,
"customTextLogin": true,
"lockoutPolicy": true,
"actions": true
}`),
), org.FeaturesSetEventMapper),
},
reduce: (&FeatureProjection{}).reduceFeatureSet,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
projection: FeatureTable,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, creation_date, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
anyArg{},
uint64(15),
false,
"TierName",
"TierDescription",
domain.FeaturesStateActive,
"StateDescription",
time.Nanosecond,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
},
},
},
},
},
},
{
name: "org.reduceFeatureRemoved",
reduce: (&FeatureProjection{}).reduceFeatureRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.FeaturesRemovedEventType),
org.AggregateType,
nil,
), org.FeaturesRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
projection: FeatureTable,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM zitadel.projections.features WHERE (aggregate_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
},
},
{
name: "iam.reduceFeatureSet",
reduce: (&FeatureProjection{}).reduceFeatureSet,
args: args{
event: getEvent(testEvent(
repository.EventType(iam.FeaturesSetEventType),
iam.AggregateType,
[]byte(`{
"tierName": "TierName",
"tierDescription": "TierDescription",
"state": 1,
"stateDescription": "StateDescription",
"auditLogRetention": 1,
"loginPolicyFactors": true,
"loginPolicyIDP": true,
"loginPolicyPasswordless": true,
"loginPolicyRegistration": true,
"loginPolicyUsernameLogin": true,
"loginPolicyPasswordReset": true,
"passwordComplexityPolicy": true,
"labelPolicyPrivateLabel": true,
"labelPolicyWatermark": true,
"customDomain": true,
"privacyPolicy": true,
"metadataUser": true,
"customTextMessage": true,
"customTextLogin": true,
"lockoutPolicy": true,
"actions": true
}`),
), iam.FeaturesSetEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("iam"),
sequence: 15,
previousSequence: 10,
projection: FeatureTable,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, creation_date, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
anyArg{},
uint64(15),
true,
"TierName",
"TierDescription",
domain.FeaturesStateActive,
"StateDescription",
time.Nanosecond,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
},
},
},
},
},
},
}
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)
})
}
}

View File

@@ -51,6 +51,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co
NewMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"]))
NewMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"]))
NewCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"]))
NewFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["features"]))
return nil
}

View File

@@ -0,0 +1,32 @@
CREATE TABLE zitadel.projections.features
(
aggregate_id TEXT NOT NULL,
creation_date TIMESTAMPTZ NULL,
change_date TIMESTAMPTZ NULL,
sequence INT8 NULL,
is_default BOOLEAN,
tier_name TEXT,
tier_description TEXT,
state INT2 NULL,
state_description TEXT,
audit_log_retention BIGINT,
login_policy_factors BOOLEAN,
login_policy_idp BOOLEAN,
login_policy_passwordless BOOLEAN,
login_policy_registration BOOLEAN,
login_policy_username_login BOOLEAN,
login_policy_password_reset BOOLEAN,
password_complexity_policy BOOLEAN,
label_policy_private_label BOOLEAN,
label_policy_watermark BOOLEAN,
custom_domain BOOLEAN,
privacy_policy BOOLEAN,
metadata_user BOOLEAN,
custom_text_message BOOLEAN,
custom_text_login BOOLEAN,
lockout_policy BOOLEAN,
actions BOOLEAN,
PRIMARY KEY (aggregate_id)
);