diff --git a/internal/query/projection/custom_text.go b/internal/query/projection/custom_text.go
new file mode 100644
index 0000000000..69337901d1
--- /dev/null
+++ b/internal/query/projection/custom_text.go
@@ -0,0 +1,149 @@
+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"
+ "github.com/caos/zitadel/internal/repository/org"
+ "github.com/caos/zitadel/internal/repository/policy"
+)
+
+type CustomTextProjection struct {
+ crdb.StatementHandler
+}
+
+const (
+ CustomTextTable = "zitadel.projections.custom_texts"
+
+ CustomTextAggregateIDCol = "aggregate_id"
+ CustomTextCreationDateCol = "creation_date"
+ CustomTextChangeDateCol = "change_date"
+ CustomTextSequenceCol = "sequence"
+ CustomTextIsDefaultCol = "is_default"
+ CustomTextTemplateCol = "template"
+ CustomTextLanguageCol = "language"
+ CustomTextKeyCol = "key"
+ CustomTextTextCol = "text"
+)
+
+func NewCustomTextProjection(ctx context.Context, config crdb.StatementHandlerConfig) *CustomTextProjection {
+ p := &CustomTextProjection{}
+ config.ProjectionName = CustomTextTable
+ config.Reducers = p.reducers()
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *CustomTextProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: org.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: org.CustomTextSetEventType,
+ Reduce: p.reduceSet,
+ },
+ {
+ Event: org.CustomTextRemovedEventType,
+ Reduce: p.reduceRemoved,
+ },
+ {
+ Event: org.CustomTextTemplateRemovedEventType,
+ Reduce: p.reduceTemplateRemoved,
+ },
+ },
+ },
+ {
+ Aggregate: iam.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: iam.CustomTextSetEventType,
+ Reduce: p.reduceSet,
+ },
+ {
+ Event: iam.CustomTextRemovedEventType,
+ Reduce: p.reduceRemoved,
+ },
+ {
+ Event: iam.CustomTextTemplateRemovedEventType,
+ Reduce: p.reduceTemplateRemoved,
+ },
+ },
+ },
+ }
+}
+
+func (p *CustomTextProjection) reduceSet(event eventstore.EventReader) (*handler.Statement, error) {
+ var customTextEvent policy.CustomTextSetEvent
+ var isDefault bool
+ switch e := event.(type) {
+ case *org.CustomTextSetEvent:
+ customTextEvent = e.CustomTextSetEvent
+ isDefault = false
+ case *iam.CustomTextSetEvent:
+ customTextEvent = e.CustomTextSetEvent
+ isDefault = true
+ default:
+ logging.LogWithFields("PROJE-g0Jfs", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.CustomTextSetEventType, iam.CustomTextSetEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-KKfw4", "reduce.wrong.event.type")
+ }
+ return crdb.NewCreateStatement(
+ &customTextEvent,
+ []handler.Column{
+ handler.NewCol(CustomTextAggregateIDCol, customTextEvent.Aggregate().ID),
+ handler.NewCol(CustomTextCreationDateCol, customTextEvent.CreationDate()),
+ handler.NewCol(CustomTextChangeDateCol, customTextEvent.CreationDate()),
+ handler.NewCol(CustomTextSequenceCol, customTextEvent.Sequence()),
+ handler.NewCol(CustomTextIsDefaultCol, isDefault),
+ handler.NewCol(CustomTextTemplateCol, customTextEvent.Template),
+ handler.NewCol(CustomTextLanguageCol, customTextEvent.Language.String()),
+ handler.NewCol(CustomTextKeyCol, customTextEvent.Key),
+ handler.NewCol(CustomTextTextCol, customTextEvent.Text),
+ }), nil
+}
+
+func (p *CustomTextProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) {
+ var customTextEvent policy.CustomTextRemovedEvent
+ switch e := event.(type) {
+ case *org.CustomTextRemovedEvent:
+ customTextEvent = e.CustomTextRemovedEvent
+ case *iam.CustomTextRemovedEvent:
+ customTextEvent = e.CustomTextRemovedEvent
+ default:
+ logging.LogWithFields("PROJE-2Nigw", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.CustomTextRemovedEventType, iam.CustomTextRemovedEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-n9wJg", "reduce.wrong.event.type")
+ }
+ return crdb.NewDeleteStatement(
+ &customTextEvent,
+ []handler.Condition{
+ handler.NewCond(CustomTextAggregateIDCol, customTextEvent.Aggregate().ID),
+ handler.NewCond(CustomTextTemplateCol, customTextEvent.Template),
+ handler.NewCond(CustomTextKeyCol, customTextEvent.Key),
+ handler.NewCond(CustomTextLanguageCol, customTextEvent.Language),
+ }), nil
+}
+
+func (p *CustomTextProjection) reduceTemplateRemoved(event eventstore.EventReader) (*handler.Statement, error) {
+ var customTextEvent policy.CustomTextTemplateRemovedEvent
+ switch e := event.(type) {
+ case *org.CustomTextTemplateRemovedEvent:
+ customTextEvent = e.CustomTextTemplateRemovedEvent
+ case *iam.CustomTextTemplateRemovedEvent:
+ customTextEvent = e.CustomTextTemplateRemovedEvent
+ default:
+ logging.LogWithFields("PROJE-J9wfg", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.CustomTextTemplateRemovedEventType, iam.CustomTextTemplateRemovedEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-29iPf", "reduce.wrong.event.type")
+ }
+ return crdb.NewDeleteStatement(
+ &customTextEvent,
+ []handler.Condition{
+ handler.NewCond(CustomTextAggregateIDCol, customTextEvent.Aggregate().ID),
+ handler.NewCond(CustomTextTemplateCol, customTextEvent.Template),
+ handler.NewCond(CustomTextLanguageCol, customTextEvent.Language),
+ }), nil
+}
diff --git a/internal/query/projection/custom_text_test.go b/internal/query/projection/custom_text_test.go
new file mode 100644
index 0000000000..733728f073
--- /dev/null
+++ b/internal/query/projection/custom_text_test.go
@@ -0,0 +1,253 @@
+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"
+ "github.com/caos/zitadel/internal/repository/org"
+ "golang.org/x/text/language"
+)
+
+func TestCustomTextProjection_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.reduceSet",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&CustomTextProjection{}).reduceSet,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO zitadel.projections.custom_texts (aggregate_id, creation_date, change_date, sequence, is_default, template, language, key, text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ false,
+ "InitCode",
+ "en",
+ "Text",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved",
+ reduce: (&CustomTextProjection{}).reduceRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.custom_texts WHERE (aggregate_id = $1) AND (template = $2) AND (key = $3) AND (language = $4)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "InitCode",
+ "Text",
+ language.English,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceTemplateRemoved",
+ reduce: (&CustomTextProjection{}).reduceTemplateRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextTemplateRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextTemplateRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.custom_texts WHERE (aggregate_id = $1) AND (template = $2) AND (language = $3)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "InitCode",
+ language.English,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceAdded",
+ reduce: (&CustomTextProjection{}).reduceSet,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.CustomTextSetEventType),
+ iam.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), iam.CustomTextSetEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO zitadel.projections.custom_texts (aggregate_id, creation_date, change_date, sequence, is_default, template, language, key, text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ true,
+ "InitCode",
+ "en",
+ "Text",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceRemoved",
+ reduce: (&CustomTextProjection{}).reduceRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.CustomTextTemplateRemovedEventType),
+ iam.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), iam.CustomTextRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.custom_texts WHERE (aggregate_id = $1) AND (template = $2) AND (key = $3) AND (language = $4)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "InitCode",
+ "Text",
+ language.English,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceTemplateRemoved",
+ reduce: (&CustomTextProjection{}).reduceTemplateRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.CustomTextTemplateRemovedEventType),
+ iam.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), iam.CustomTextTemplateRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: CustomTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.custom_texts WHERE (aggregate_id = $1) AND (template = $2) AND (language = $3)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "InitCode",
+ language.English,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ 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/mail_template.go b/internal/query/projection/mail_template.go
new file mode 100644
index 0000000000..c3be64e6a9
--- /dev/null
+++ b/internal/query/projection/mail_template.go
@@ -0,0 +1,140 @@
+package projection
+
+import (
+ "context"
+
+ "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/iam"
+ "github.com/caos/zitadel/internal/repository/org"
+ "github.com/caos/zitadel/internal/repository/policy"
+)
+
+type MailTemplateProjection struct {
+ crdb.StatementHandler
+}
+
+const (
+ MailTemplateTable = "zitadel.projections.mail_templates"
+
+ MailTemplateAggregateIDCol = "aggregate_id"
+ MailTemplateCreationDateCol = "creation_date"
+ MailTemplateChangeDateCol = "change_date"
+ MailTemplateSequenceCol = "sequence"
+ MailTemplateStateCol = "state"
+ MailTemplateTemplateCol = "template"
+ MailTemplateIsDefaultCol = "is_default"
+)
+
+func NewMailTemplateProjection(ctx context.Context, config crdb.StatementHandlerConfig) *MailTemplateProjection {
+ p := &MailTemplateProjection{}
+ config.ProjectionName = MailTemplateTable
+ config.Reducers = p.reducers()
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *MailTemplateProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: org.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: org.MailTemplateAddedEventType,
+ Reduce: p.reduceAdded,
+ },
+ {
+ Event: org.MailTemplateChangedEventType,
+ Reduce: p.reduceChanged,
+ },
+ {
+ Event: org.MailTemplateRemovedEventType,
+ Reduce: p.reduceRemoved,
+ },
+ },
+ },
+ {
+ Aggregate: iam.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: iam.MailTemplateAddedEventType,
+ Reduce: p.reduceAdded,
+ },
+ {
+ Event: iam.MailTemplateChangedEventType,
+ Reduce: p.reduceChanged,
+ },
+ },
+ },
+ }
+}
+
+func (p *MailTemplateProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) {
+ var templateEvent policy.MailTemplateAddedEvent
+ var isDefault bool
+ switch e := event.(type) {
+ case *org.MailTemplateAddedEvent:
+ templateEvent = e.MailTemplateAddedEvent
+ isDefault = false
+ case *iam.MailTemplateAddedEvent:
+ templateEvent = e.MailTemplateAddedEvent
+ isDefault = true
+ default:
+ logging.LogWithFields("PROJE-94jfG", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.MailTemplateAddedEventType, iam.MailTemplateAddedEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-0pJ3f", "reduce.wrong.event.type")
+ }
+ return crdb.NewCreateStatement(
+ &templateEvent,
+ []handler.Column{
+ handler.NewCol(MailTemplateAggregateIDCol, templateEvent.Aggregate().ID),
+ handler.NewCol(MailTemplateCreationDateCol, templateEvent.CreationDate()),
+ handler.NewCol(MailTemplateChangeDateCol, templateEvent.CreationDate()),
+ handler.NewCol(MailTemplateSequenceCol, templateEvent.Sequence()),
+ handler.NewCol(MailTemplateStateCol, domain.PolicyStateActive),
+ handler.NewCol(MailTemplateIsDefaultCol, isDefault),
+ handler.NewCol(MailTemplateTemplateCol, templateEvent.Template),
+ }), nil
+}
+
+func (p *MailTemplateProjection) reduceChanged(event eventstore.EventReader) (*handler.Statement, error) {
+ var policyEvent policy.MailTemplateChangedEvent
+ switch e := event.(type) {
+ case *org.MailTemplateChangedEvent:
+ policyEvent = e.MailTemplateChangedEvent
+ case *iam.MailTemplateChangedEvent:
+ policyEvent = e.MailTemplateChangedEvent
+ default:
+ logging.LogWithFields("PROJE-02J9f", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.MailTemplateChangedEventType, iam.MailTemplateChangedEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-gJ03f", "reduce.wrong.event.type")
+ }
+ cols := []handler.Column{
+ handler.NewCol(MailTemplateChangeDateCol, policyEvent.CreationDate()),
+ handler.NewCol(MailTemplateSequenceCol, policyEvent.Sequence()),
+ }
+ if policyEvent.Template != nil {
+ cols = append(cols, handler.NewCol(MailTemplateTemplateCol, *policyEvent.Template))
+ }
+ return crdb.NewUpdateStatement(
+ &policyEvent,
+ cols,
+ []handler.Condition{
+ handler.NewCond(MailTemplateAggregateIDCol, policyEvent.Aggregate().ID),
+ }), nil
+}
+
+func (p *MailTemplateProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) {
+ policyEvent, ok := event.(*org.MailTemplateRemovedEvent)
+ if !ok {
+ logging.LogWithFields("PROJE-2m0fp", "seq", event.Sequence(), "expectedType", org.MailTemplateRemovedEventType).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-3jJGs", "reduce.wrong.event.type")
+ }
+ return crdb.NewDeleteStatement(
+ policyEvent,
+ []handler.Condition{
+ handler.NewCond(MailTemplateAggregateIDCol, policyEvent.Aggregate().ID),
+ }), nil
+}
diff --git a/internal/query/projection/mail_template_test.go b/internal/query/projection/mail_template_test.go
new file mode 100644
index 0000000000..12a0a53afb
--- /dev/null
+++ b/internal/query/projection/mail_template_test.go
@@ -0,0 +1,200 @@
+package projection
+
+import (
+ "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/iam"
+ "github.com/caos/zitadel/internal/repository/org"
+)
+
+func TestMailTemplateProjection_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.reduceAdded",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MailTemplateAddedEventType),
+ org.AggregateType,
+ []byte(`{
+ "template": "PHRhYmxlPjwvdGFibGU+"
+ }`),
+ ), org.MailTemplateAddedEventMapper),
+ },
+ reduce: (&MailTemplateProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MailTemplateTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO zitadel.projections.mail_templates (aggregate_id, creation_date, change_date, sequence, state, is_default, template) VALUES ($1, $2, $3, $4, $5, $6, $7)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ false,
+ []byte("
"),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceChanged",
+ reduce: (&MailTemplateProjection{}).reduceChanged,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MailTemplateChangedEventType),
+ org.AggregateType,
+ []byte(`{
+ "template": "PHRhYmxlPjwvdGFibGU+"
+ }`),
+ ), org.MailTemplateChangedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MailTemplateTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.mail_templates SET (change_date, sequence, template) = ($1, $2, $3) WHERE (aggregate_id = $4)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ []byte(""),
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved",
+ reduce: (&MailTemplateProjection{}).reduceRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MailTemplateRemovedEventType),
+ org.AggregateType,
+ nil,
+ ), org.MailTemplateRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MailTemplateTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.mail_templates WHERE (aggregate_id = $1)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceAdded",
+ reduce: (&MailTemplateProjection{}).reduceAdded,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.MailTemplateAddedEventType),
+ iam.AggregateType,
+ []byte(`{
+ "template": "PHRhYmxlPjwvdGFibGU+"
+ }`),
+ ), iam.MailTemplateAddedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MailTemplateTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO zitadel.projections.mail_templates (aggregate_id, creation_date, change_date, sequence, state, is_default, template) VALUES ($1, $2, $3, $4, $5, $6, $7)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ true,
+ []byte(""),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceChanged",
+ reduce: (&MailTemplateProjection{}).reduceChanged,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.MailTemplateChangedEventType),
+ iam.AggregateType,
+ []byte(`{
+ "template": "PHRhYmxlPjwvdGFibGU+"
+ }`),
+ ), iam.MailTemplateChangedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MailTemplateTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.mail_templates SET (change_date, sequence, template) = ($1, $2, $3) WHERE (aggregate_id = $4)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ []byte(""),
+ "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/message_text_test.go b/internal/query/projection/message_text_test.go
new file mode 100644
index 0000000000..0f7f3059d5
--- /dev/null
+++ b/internal/query/projection/message_text_test.go
@@ -0,0 +1,673 @@
+package projection
+
+import (
+ "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/iam"
+ "github.com/caos/zitadel/internal/repository/org"
+ "golang.org/x/text/language"
+)
+
+func TestMessageTextProjection_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.reduceAdded.Title",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Title",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, title) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.PreHeader",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "PreHeader",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, pre_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.Subject",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Subject",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, subject) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.Greeting",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Greeting",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, greeting) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.Text",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.ButtonText",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "ButtonText",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, button_text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceAdded.Footer",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextSetEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Footer",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), org.CustomTextSetEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, footer_text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.Title",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Title",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, title) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.PreHeader",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "PreHeader",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, pre_header) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.Subject",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Subject",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, subject) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.Greeting",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Greeting",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, greeting) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.Text",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Text",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, text) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.ButtonText",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "ButtonText",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, button_text) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved.Footer",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Footer",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, footer_text) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceRemoved",
+ reduce: (&MessageTextProjection{}).reduceTemplateRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.CustomTextTemplateRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "key": "Title",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), org.CustomTextTemplateRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM zitadel.projections.message_texts WHERE (aggregate_id = $1) AND (type = $2) AND (language = $3)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "InitCode",
+ language.English,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceAdded",
+ reduce: (&MessageTextProjection{}).reduceAdded,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.CustomTextSetEventType),
+ iam.AggregateType,
+ []byte(`{
+ "key": "Title",
+ "language": "en",
+ "template": "InitCode",
+ "text": "Test"
+ }`),
+ ), iam.CustomTextSetEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPSERT INTO zitadel.projections.message_texts (aggregate_id, creation_date, change_date, sequence, state, type, language, title) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ domain.PolicyStateActive,
+ "InitCode",
+ "en",
+ "Test",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "iam.reduceRemoved.Title",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(iam.CustomTextRemovedEventType),
+ iam.AggregateType,
+ []byte(`{
+ "key": "Title",
+ "language": "en",
+ "template": "InitCode"
+ }`),
+ ), iam.CustomTextRemovedEventMapper),
+ },
+ reduce: (&MessageTextProjection{}).reduceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("iam"),
+ sequence: 15,
+ previousSequence: 10,
+ projection: MessageTextTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE zitadel.projections.message_texts SET (change_date, sequence, title) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (type = $5) AND (language = $6)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ "",
+ "agg-id",
+ "InitCode",
+ "en",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ 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/message_texts.go b/internal/query/projection/message_texts.go
new file mode 100644
index 0000000000..6ab339e30e
--- /dev/null
+++ b/internal/query/projection/message_texts.go
@@ -0,0 +1,234 @@
+package projection
+
+import (
+ "context"
+
+ "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/iam"
+ "github.com/caos/zitadel/internal/repository/org"
+ "github.com/caos/zitadel/internal/repository/policy"
+)
+
+type MessageTextProjection struct {
+ crdb.StatementHandler
+}
+
+const (
+ MessageTextTable = "zitadel.projections.message_texts"
+
+ MessageTextAggregateIDCol = "aggregate_id"
+ MessageTextCreationDateCol = "creation_date"
+ MessageTextChangeDateCol = "change_date"
+ MessageTextSequenceCol = "sequence"
+ MessageTextStateCol = "state"
+ MessageTextTypeCol = "type"
+ MessageTextLanguageCol = "language"
+ MessageTextTitleCol = "title"
+ MessageTextPreHeaderCol = "pre_header"
+ MessageTextSubjectCol = "subject"
+ MessageTextGreetingCol = "greeting"
+ MessageTextTextCol = "text"
+ MessageTextButtonTextCol = "button_text"
+ MessageTextFooterCol = "footer_text"
+)
+
+func NewMessageTextProjection(ctx context.Context, config crdb.StatementHandlerConfig) *MessageTextProjection {
+ p := &MessageTextProjection{}
+ config.ProjectionName = MessageTextTable
+ config.Reducers = p.reducers()
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *MessageTextProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: org.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: org.CustomTextSetEventType,
+ Reduce: p.reduceAdded,
+ },
+ {
+ Event: org.CustomTextRemovedEventType,
+ Reduce: p.reduceRemoved,
+ },
+ {
+ Event: org.CustomTextTemplateRemovedEventType,
+ Reduce: p.reduceTemplateRemoved,
+ },
+ },
+ },
+ {
+ Aggregate: iam.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: iam.CustomTextSetEventType,
+ Reduce: p.reduceAdded,
+ },
+ {
+ Event: iam.CustomTextRemovedEventType,
+ Reduce: p.reduceRemoved,
+ },
+ {
+ Event: iam.CustomTextTemplateRemovedEventType,
+ Reduce: p.reduceTemplateRemoved,
+ },
+ },
+ },
+ }
+}
+
+func (p *MessageTextProjection) reduceAdded(event eventstore.EventReader) (*handler.Statement, error) {
+ var templateEvent policy.CustomTextSetEvent
+ switch e := event.(type) {
+ case *org.CustomTextSetEvent:
+ templateEvent = e.CustomTextSetEvent
+ case *iam.CustomTextSetEvent:
+ templateEvent = e.CustomTextSetEvent
+ default:
+ logging.LogWithFields("PROJE-2N9fg", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.CustomTextSetEventType, iam.CustomTextSetEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-2n90r", "reduce.wrong.event.type")
+ }
+ if !isMessageTemplate(templateEvent.Template) {
+ return nil, nil
+ }
+
+ cols := []handler.Column{
+ handler.NewCol(MessageTextAggregateIDCol, templateEvent.Aggregate().ID),
+ handler.NewCol(MessageTextCreationDateCol, templateEvent.CreationDate()),
+ handler.NewCol(MessageTextChangeDateCol, templateEvent.CreationDate()),
+ handler.NewCol(MessageTextSequenceCol, templateEvent.Sequence()),
+ handler.NewCol(MessageTextStateCol, domain.PolicyStateActive),
+ handler.NewCol(MessageTextTypeCol, templateEvent.Template),
+ handler.NewCol(MessageTextLanguageCol, templateEvent.Language.String()),
+ }
+ if isTitle(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextTitleCol, templateEvent.Text))
+ }
+ if isPreHeader(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextPreHeaderCol, templateEvent.Text))
+ }
+ if isSubject(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextSubjectCol, templateEvent.Text))
+ }
+ if isGreeting(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextGreetingCol, templateEvent.Text))
+ }
+ if isText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextTextCol, templateEvent.Text))
+ }
+ if isButtonText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextButtonTextCol, templateEvent.Text))
+ }
+ if isFooterText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextFooterCol, templateEvent.Text))
+ }
+ return crdb.NewUpsertStatement(
+ &templateEvent,
+ cols), nil
+}
+
+func (p *MessageTextProjection) reduceRemoved(event eventstore.EventReader) (*handler.Statement, error) {
+ var templateEvent policy.CustomTextRemovedEvent
+ switch e := event.(type) {
+ case *org.CustomTextRemovedEvent:
+ templateEvent = e.CustomTextRemovedEvent
+ case *iam.CustomTextRemovedEvent:
+ templateEvent = e.CustomTextRemovedEvent
+ default:
+ logging.LogWithFields("PROJE-3m022", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.CustomTextRemovedEventType, iam.CustomTextRemovedEventType}).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-fm0ge", "reduce.wrong.event.type")
+ }
+ if !isMessageTemplate(templateEvent.Template) {
+ return nil, nil
+ }
+ cols := []handler.Column{
+ handler.NewCol(MessageTextChangeDateCol, templateEvent.CreationDate()),
+ handler.NewCol(MessageTextSequenceCol, templateEvent.Sequence()),
+ }
+ if isTitle(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextTitleCol, ""))
+ }
+ if isPreHeader(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextPreHeaderCol, ""))
+ }
+ if isSubject(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextSubjectCol, ""))
+ }
+ if isGreeting(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextGreetingCol, ""))
+ }
+ if isText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextTextCol, ""))
+ }
+ if isButtonText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextButtonTextCol, ""))
+ }
+ if isFooterText(templateEvent.Key) {
+ cols = append(cols, handler.NewCol(MessageTextFooterCol, ""))
+ }
+ return crdb.NewUpdateStatement(
+ &templateEvent,
+ cols,
+ []handler.Condition{
+ handler.NewCond(MessageTextAggregateIDCol, templateEvent.Aggregate().ID),
+ handler.NewCond(MessageTextTypeCol, templateEvent.Template),
+ handler.NewCond(MessageTextLanguageCol, templateEvent.Language.String()),
+ },
+ ), nil
+}
+
+func (p *MessageTextProjection) reduceTemplateRemoved(event eventstore.EventReader) (*handler.Statement, error) {
+ templateEvent, ok := event.(*org.CustomTextTemplateRemovedEvent)
+ if !ok {
+ logging.LogWithFields("PROJE-m03ng", "seq", event.Sequence(), "expectedType", org.CustomTextTemplateRemovedEventType).Error("wrong event type")
+ return nil, errors.ThrowInvalidArgument(nil, "PROJE-2n9rs", "reduce.wrong.event.type")
+ }
+ if !isMessageTemplate(templateEvent.Template) {
+ return nil, nil
+ }
+ return crdb.NewDeleteStatement(
+ templateEvent,
+ []handler.Condition{
+ handler.NewCond(MessageTextAggregateIDCol, templateEvent.Aggregate().ID),
+ handler.NewCond(MessageTextTypeCol, templateEvent.Template),
+ handler.NewCond(MessageTextLanguageCol, templateEvent.Language),
+ },
+ ), nil
+}
+
+func isMessageTemplate(template string) bool {
+ return template == domain.InitCodeMessageType ||
+ template == domain.PasswordResetMessageType ||
+ template == domain.VerifyEmailMessageType ||
+ template == domain.VerifyPhoneMessageType ||
+ template == domain.DomainClaimedMessageType ||
+ template == domain.PasswordlessRegistrationMessageType
+}
+func isTitle(key string) bool {
+ return key == domain.MessageTitle
+}
+func isPreHeader(key string) bool {
+ return key == domain.MessagePreHeader
+}
+func isSubject(key string) bool {
+ return key == domain.MessageSubject
+}
+func isGreeting(key string) bool {
+ return key == domain.MessageGreeting
+}
+func isText(key string) bool {
+ return key == domain.MessageText
+}
+func isButtonText(key string) bool {
+ return key == domain.MessageButtonText
+}
+func isFooterText(key string) bool {
+ return key == domain.MessageFooterText
+}
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index e8deea60f0..26b015908a 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -48,6 +48,9 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co
NewOrgDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_domains"]))
NewLoginPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["login_policies"]))
NewIDPProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idps"]))
+ NewMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"]))
+ NewMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"]))
+ NewCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"]))
return nil
}
diff --git a/migrations/cockroach/V1.85__messages.sql b/migrations/cockroach/V1.85__messages.sql
new file mode 100644
index 0000000000..cd448faa98
--- /dev/null
+++ b/migrations/cockroach/V1.85__messages.sql
@@ -0,0 +1,50 @@
+CREATE TABLE zitadel.projections.mail_templates (
+ aggregate_id TEXT NOT NULL,
+
+ creation_date TIMESTAMPTZ,
+ change_date TIMESTAMPTZ,
+ state SMALLINT,
+ sequence BIGINT,
+ is_default BOOLEAN,
+
+ template BYTES,
+
+ PRIMARY KEY (aggregate_id)
+);
+
+CREATE TABLE zitadel.projections.message_texts (
+ aggregate_id TEXT,
+
+ creation_date TIMESTAMPTZ,
+ change_date TIMESTAMPTZ,
+ state SMALLINT,
+ sequence BIGINT,
+
+ type TEXT,
+ language TEXT,
+ title TEXT,
+ pre_header TEXT,
+ subject TEXT,
+ greeting TEXT,
+ text TEXT,
+ button_text TEXT,
+ footer_text TEXT,
+
+ PRIMARY KEY (aggregate_id, type, language)
+);
+
+CREATE TABLE zitadel.projections.custom_texts (
+ aggregate_id TEXT,
+
+ creation_date TIMESTAMPTZ,
+ change_date TIMESTAMPTZ,
+ sequence BIGINT,
+ is_default BOOLEAN,
+
+ template TEXT,
+ language TEXT,
+ key TEXT,
+ text TEXT,
+
+ PRIMARY KEY (aggregate_id, template, key, language)
+);
\ No newline at end of file