mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 07:47:32 +00:00
chore: move the go code into a subfolder
This commit is contained in:
7
apps/api/internal/v2/avatar/added.go
Normal file
7
apps/api/internal/v2/avatar/added.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package avatar
|
||||
|
||||
const AvatarAddedTypeSuffix = ".avatar.added"
|
||||
|
||||
type AddedPayload struct {
|
||||
StoreKey string `json:"storeKey"`
|
||||
}
|
7
apps/api/internal/v2/avatar/removed.go
Normal file
7
apps/api/internal/v2/avatar/removed.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package avatar
|
||||
|
||||
const AvatarRemovedTypeSuffix = ".avatar.removed"
|
||||
|
||||
type RemovedPayload struct {
|
||||
StoreKey string `json:"storeKey"`
|
||||
}
|
33
apps/api/internal/v2/database/filter.go
Normal file
33
apps/api/internal/v2/database/filter.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
type Condition interface {
|
||||
Write(stmt *Statement, columnName string)
|
||||
}
|
||||
|
||||
type Filter[C compare, V value] struct {
|
||||
comp C
|
||||
value V
|
||||
}
|
||||
|
||||
func (f Filter[C, V]) Write(stmt *Statement, columnName string) {
|
||||
prepareWrite(stmt, columnName, f.comp)
|
||||
stmt.WriteArg(f.value)
|
||||
}
|
||||
|
||||
func prepareWrite[C compare](stmt *Statement, columnName string, comp C) {
|
||||
stmt.WriteString(columnName)
|
||||
stmt.WriteRune(' ')
|
||||
stmt.WriteString(comp.String())
|
||||
stmt.WriteRune(' ')
|
||||
}
|
||||
|
||||
type compare interface {
|
||||
numberCompare | textCompare | listCompare
|
||||
String() string
|
||||
}
|
||||
|
||||
type value interface {
|
||||
number | text
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// number | text | placeholder
|
||||
}
|
57
apps/api/internal/v2/database/list_filter.go
Normal file
57
apps/api/internal/v2/database/list_filter.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package database
|
||||
|
||||
import "github.com/zitadel/logging"
|
||||
|
||||
type ListFilter[V value] struct {
|
||||
comp listCompare
|
||||
list []V
|
||||
}
|
||||
|
||||
func NewListEquals[V value](list ...V) *ListFilter[V] {
|
||||
return newListFilter[V](listEqual, list)
|
||||
}
|
||||
|
||||
func NewListContains[V value](list ...V) *ListFilter[V] {
|
||||
return newListFilter[V](listContain, list)
|
||||
}
|
||||
|
||||
func NewListNotContains[V value](list ...V) *ListFilter[V] {
|
||||
return newListFilter[V](listNotContain, list)
|
||||
}
|
||||
|
||||
func newListFilter[V value](comp listCompare, list []V) *ListFilter[V] {
|
||||
return &ListFilter[V]{
|
||||
comp: comp,
|
||||
list: list,
|
||||
}
|
||||
}
|
||||
|
||||
func (f ListFilter[V]) Write(stmt *Statement, columnName string) {
|
||||
if len(f.list) == 0 {
|
||||
logging.WithFields("column", columnName).Debug("skip list filter because no entries defined")
|
||||
return
|
||||
}
|
||||
if f.comp == listNotContain {
|
||||
stmt.WriteString("NOT(")
|
||||
}
|
||||
stmt.WriteString(columnName)
|
||||
stmt.WriteString(" = ")
|
||||
if f.comp != listEqual {
|
||||
stmt.WriteString("ANY(")
|
||||
}
|
||||
stmt.WriteArg(f.list)
|
||||
if f.comp != listEqual {
|
||||
stmt.WriteString(")")
|
||||
}
|
||||
if f.comp == listNotContain {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
type listCompare uint8
|
||||
|
||||
const (
|
||||
listEqual listCompare = iota
|
||||
listContain
|
||||
listNotContain
|
||||
)
|
122
apps/api/internal/v2/database/list_filter_test.go
Normal file
122
apps/api/internal/v2/database/list_filter_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewListConstructors(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t ...string) *ListFilter[string]
|
||||
t []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ListFilter[string]
|
||||
}{
|
||||
{
|
||||
name: "NewListEquals",
|
||||
args: args{
|
||||
constructor: NewListEquals[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: &ListFilter[string]{
|
||||
comp: listEqual,
|
||||
list: []string{"as", "df"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewListContains",
|
||||
args: args{
|
||||
constructor: NewListContains[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: &ListFilter[string]{
|
||||
comp: listContain,
|
||||
list: []string{"as", "df"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewListNotContains",
|
||||
args: args{
|
||||
constructor: NewListNotContains[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: &ListFilter[string]{
|
||||
comp: listNotContain,
|
||||
list: []string{"as", "df"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.args.constructor(tt.args.t...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("number constructor = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewListConditionWrite(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t ...string) *ListFilter[string]
|
||||
t []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want wantQuery
|
||||
}{
|
||||
{
|
||||
name: "ListEquals",
|
||||
args: args{
|
||||
constructor: NewListEquals[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test = $1",
|
||||
args: []any{[]string{"as", "df"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ListContains",
|
||||
args: args{
|
||||
constructor: NewListContains[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test = ANY($1)",
|
||||
args: []any{[]string{"as", "df"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ListNotContains",
|
||||
args: args{
|
||||
constructor: NewListNotContains[string],
|
||||
t: []string{"as", "df"},
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "NOT(test = ANY($1))",
|
||||
args: []any{[]string{"as", "df"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{
|
||||
constructor: NewListNotContains[string],
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "",
|
||||
args: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var stmt Statement
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.constructor(tt.args.t...).Write(&stmt, "test")
|
||||
assertQuery(t, &stmt, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
147
apps/api/internal/v2/database/mock/sql_mock.go
Normal file
147
apps/api/internal/v2/database/mock/sql_mock.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
type SQLMock struct {
|
||||
DB *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
type Expectation func(m sqlmock.Sqlmock)
|
||||
|
||||
func NewSQLMock(t *testing.T, expectations ...Expectation) *SQLMock {
|
||||
db, mock, err := sqlmock.New(
|
||||
sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
|
||||
sqlmock.ValueConverterOption(new(TypeConverter)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("create mock failed", err)
|
||||
}
|
||||
|
||||
for _, expectation := range expectations {
|
||||
expectation(mock)
|
||||
}
|
||||
|
||||
return &SQLMock{
|
||||
DB: db,
|
||||
mock: mock,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQLMock) Assert(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if err := m.mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("expectations not met: %v", err)
|
||||
}
|
||||
|
||||
m.DB.Close()
|
||||
}
|
||||
|
||||
func ExpectBegin(err error) Expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
e := m.ExpectBegin()
|
||||
if err != nil {
|
||||
e.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectCommit(err error) Expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
e := m.ExpectCommit()
|
||||
if err != nil {
|
||||
e.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ExecOpt func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec
|
||||
|
||||
func WithExecArgs(args ...driver.Value) ExecOpt {
|
||||
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
|
||||
return e.WithArgs(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExecErr(err error) ExecOpt {
|
||||
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
|
||||
return e.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExecNoRowsAffected() ExecOpt {
|
||||
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
|
||||
return e.WillReturnResult(driver.ResultNoRows)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExecRowsAffected(affected driver.RowsAffected) ExecOpt {
|
||||
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
|
||||
return e.WillReturnResult(affected)
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectExec(stmt string, opts ...ExecOpt) Expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
e := m.ExpectExec(stmt)
|
||||
for _, opt := range opts {
|
||||
e = opt(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type QueryOpt func(m sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery
|
||||
|
||||
func WithQueryArgs(args ...driver.Value) QueryOpt {
|
||||
return func(_ sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
|
||||
return e.WithArgs(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithQueryErr(err error) QueryOpt {
|
||||
return func(_ sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
|
||||
return e.WillReturnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func WithQueryResult(columns []string, rows [][]driver.Value) QueryOpt {
|
||||
return func(m sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
|
||||
mockedRows := m.NewRows(columns)
|
||||
for _, row := range rows {
|
||||
mockedRows = mockedRows.AddRow(row...)
|
||||
}
|
||||
return e.WillReturnRows(mockedRows)
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectQuery(stmt string, opts ...QueryOpt) Expectation {
|
||||
return func(m sqlmock.Sqlmock) {
|
||||
e := m.ExpectQuery(stmt)
|
||||
for _, opt := range opts {
|
||||
e = opt(m, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AnyType[T interface{}] struct{}
|
||||
|
||||
// Match satisfies sqlmock.Argument interface
|
||||
func (a AnyType[T]) Match(v driver.Value) bool {
|
||||
return reflect.TypeOf(new(T)).Elem().Kind().String() == reflect.TypeOf(v).Kind().String()
|
||||
}
|
||||
|
||||
var NilArg nilArgument
|
||||
|
||||
type nilArgument struct{}
|
||||
|
||||
func (a nilArgument) Match(v driver.Value) bool {
|
||||
return reflect.ValueOf(v).IsNil()
|
||||
}
|
78
apps/api/internal/v2/database/mock/type_converter.go
Normal file
78
apps/api/internal/v2/database/mock/type_converter.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ driver.ValueConverter = (*TypeConverter)(nil)
|
||||
|
||||
type TypeConverter struct{}
|
||||
|
||||
// ConvertValue converts a value to a driver Value.
|
||||
func (s TypeConverter) ConvertValue(v any) (driver.Value, error) {
|
||||
if driver.IsValue(v) {
|
||||
return v, nil
|
||||
}
|
||||
value := reflect.ValueOf(v)
|
||||
|
||||
if rawMessage, ok := v.(json.RawMessage); ok {
|
||||
return convertBytes(rawMessage), nil
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Slice {
|
||||
//nolint: exhaustive
|
||||
// only defined types
|
||||
switch value.Type().Elem().Kind() {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
return convertSigned(value), nil
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
return convertUnsigned(value), nil
|
||||
case reflect.String:
|
||||
return convertText(value), nil
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// converts a text array to valid pgx v5 representation
|
||||
func convertSigned(array reflect.Value) string {
|
||||
slice := make([]string, array.Len())
|
||||
for i := 0; i < array.Len(); i++ {
|
||||
slice[i] = strconv.FormatInt(array.Index(i).Int(), 10)
|
||||
}
|
||||
|
||||
return "{" + strings.Join(slice, ",") + "}"
|
||||
}
|
||||
|
||||
// converts a text array to valid pgx v5 representation
|
||||
func convertUnsigned(array reflect.Value) string {
|
||||
slice := make([]string, array.Len())
|
||||
for i := 0; i < array.Len(); i++ {
|
||||
slice[i] = strconv.FormatUint(array.Index(i).Uint(), 10)
|
||||
}
|
||||
|
||||
return "{" + strings.Join(slice, ",") + "}"
|
||||
}
|
||||
|
||||
// converts a text array to valid pgx v5 representation
|
||||
func convertText(array reflect.Value) string {
|
||||
slice := make([]string, array.Len())
|
||||
for i := 0; i < array.Len(); i++ {
|
||||
slice[i] = array.Index(i).String()
|
||||
}
|
||||
|
||||
return "{" + strings.Join(slice, ",") + "}"
|
||||
}
|
||||
|
||||
func convertBytes(array []byte) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(hex.EncodedLen(len(array)) + 4)
|
||||
builder.WriteString(`\x`)
|
||||
builder.Write(hex.AppendEncode(nil, array))
|
||||
return builder.String()
|
||||
}
|
101
apps/api/internal/v2/database/number_filter.go
Normal file
101
apps/api/internal/v2/database/number_filter.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type NumberFilter[N number] struct {
|
||||
Filter[numberCompare, N]
|
||||
}
|
||||
|
||||
func NewNumberEquals[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberEqual, n)
|
||||
}
|
||||
|
||||
func NewNumberAtLeast[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberAtLeast, n)
|
||||
}
|
||||
|
||||
func NewNumberAtMost[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberAtMost, n)
|
||||
}
|
||||
|
||||
func NewNumberGreater[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberGreater, n)
|
||||
}
|
||||
|
||||
func NewNumberLess[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberLess, n)
|
||||
}
|
||||
|
||||
func NewNumberUnequal[N number](n N) *NumberFilter[N] {
|
||||
return newNumberFilter(numberUnequal, n)
|
||||
}
|
||||
|
||||
func newNumberFilter[N number](comp numberCompare, n N) *NumberFilter[N] {
|
||||
return &NumberFilter[N]{
|
||||
Filter: Filter[numberCompare, N]{
|
||||
comp: comp,
|
||||
value: n,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NumberBetweenFilter combines [AtLeast] and [AtMost] comparisons
|
||||
type NumberBetweenFilter[N number] struct {
|
||||
min, max N
|
||||
}
|
||||
|
||||
func NewNumberBetween[N number](min, max N) *NumberBetweenFilter[N] {
|
||||
return &NumberBetweenFilter[N]{
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
func (f NumberBetweenFilter[N]) Write(stmt *Statement, columnName string) {
|
||||
NewNumberAtLeast[N](f.min).Write(stmt, columnName)
|
||||
stmt.WriteString(" AND ")
|
||||
NewNumberAtMost[N](f.max).Write(stmt, columnName)
|
||||
}
|
||||
|
||||
type numberCompare uint8
|
||||
|
||||
const (
|
||||
numberEqual numberCompare = iota
|
||||
numberAtLeast
|
||||
numberAtMost
|
||||
numberGreater
|
||||
numberLess
|
||||
numberUnequal
|
||||
)
|
||||
|
||||
func (c numberCompare) String() string {
|
||||
switch c {
|
||||
case numberEqual:
|
||||
return "="
|
||||
case numberAtLeast:
|
||||
return ">="
|
||||
case numberAtMost:
|
||||
return "<="
|
||||
case numberGreater:
|
||||
return ">"
|
||||
case numberLess:
|
||||
return "<"
|
||||
case numberUnequal:
|
||||
return "<>"
|
||||
default:
|
||||
logging.WithFields("compare", c).Panic("comparison type not implemented")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type number interface {
|
||||
constraints.Integer | constraints.Float | time.Time | decimal.Decimal
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// constraints.Integer | constraints.Float | time.Time | placeholder
|
||||
}
|
216
apps/api/internal/v2/database/number_filter_test.go
Normal file
216
apps/api/internal/v2/database/number_filter_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewNumberConstructors(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t int8) *NumberFilter[int8]
|
||||
t int8
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *NumberFilter[int8]
|
||||
}{
|
||||
{
|
||||
name: "NewNumberEqual",
|
||||
args: args{
|
||||
constructor: NewNumberEquals[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberEqual,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberAtLeast",
|
||||
args: args{
|
||||
constructor: NewNumberAtLeast[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberAtLeast,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberAtMost",
|
||||
args: args{
|
||||
constructor: NewNumberAtMost[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberAtMost,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberGreater",
|
||||
args: args{
|
||||
constructor: NewNumberGreater[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberGreater,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberLess",
|
||||
args: args{
|
||||
constructor: NewNumberLess[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberLess,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberUnequal",
|
||||
args: args{
|
||||
constructor: NewNumberUnequal[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: &NumberFilter[int8]{
|
||||
Filter: Filter[numberCompare, int8]{
|
||||
comp: numberUnequal,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.args.constructor(tt.args.t); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("number constructor = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNumberConditionWrite(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t int8) *NumberFilter[int8]
|
||||
t int8
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want wantQuery
|
||||
}{
|
||||
{
|
||||
name: "NewNumberEqual",
|
||||
args: args{
|
||||
constructor: NewNumberEquals[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test = $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberAtLeast",
|
||||
args: args{
|
||||
constructor: NewNumberAtLeast[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test >= $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberAtMost",
|
||||
args: args{
|
||||
constructor: NewNumberAtMost[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test <= $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberGreater",
|
||||
args: args{
|
||||
constructor: NewNumberGreater[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test > $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberLess",
|
||||
args: args{
|
||||
constructor: NewNumberLess[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test < $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewNumberUnequal",
|
||||
args: args{
|
||||
constructor: NewNumberUnequal[int8],
|
||||
t: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test <> $1",
|
||||
args: []any{int8(10)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var stmt Statement
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.constructor(tt.args.t).Write(&stmt, "test")
|
||||
assertQuery(t, &stmt, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberBetween(t *testing.T) {
|
||||
filter := NewNumberBetween[int8](10, 20)
|
||||
|
||||
if !reflect.DeepEqual(filter, &NumberBetweenFilter[int8]{min: 10, max: 20}) {
|
||||
t.Errorf("unexpected filter: %v", filter)
|
||||
}
|
||||
|
||||
var stmt Statement
|
||||
filter.Write(&stmt, "test")
|
||||
if stmt.String() != "test >= $1 AND test <= $2" {
|
||||
t.Errorf("unexpected query: got: %q", stmt.String())
|
||||
}
|
||||
|
||||
if len(stmt.Args()) != 2 {
|
||||
t.Errorf("unexpected length of args: got %d", len(stmt.Args()))
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(int8(10), stmt.Args()[0]) {
|
||||
t.Errorf("unexpected arg at position 0: want: 10, got: %v", stmt.Args()[0])
|
||||
}
|
||||
if !reflect.DeepEqual(int8(20), stmt.Args()[1]) {
|
||||
t.Errorf("unexpected arg at position 1: want: 20, got: %v", stmt.Args()[1])
|
||||
}
|
||||
}
|
17
apps/api/internal/v2/database/pagination.go
Normal file
17
apps/api/internal/v2/database/pagination.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package database
|
||||
|
||||
type Pagination struct {
|
||||
Limit uint32
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
func (p *Pagination) Write(stmt *Statement) {
|
||||
if p.Limit > 0 {
|
||||
stmt.WriteString(" LIMIT ")
|
||||
stmt.WriteArg(p.Limit)
|
||||
}
|
||||
if p.Offset > 0 {
|
||||
stmt.WriteString(" OFFSET ")
|
||||
stmt.WriteArg(p.Offset)
|
||||
}
|
||||
}
|
73
apps/api/internal/v2/database/pagination_test.go
Normal file
73
apps/api/internal/v2/database/pagination_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPagination_Write(t *testing.T) {
|
||||
type fields struct {
|
||||
Limit uint32
|
||||
Offset uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want wantQuery
|
||||
}{
|
||||
{
|
||||
name: "no values",
|
||||
fields: fields{
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "",
|
||||
args: []any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
fields: fields{
|
||||
Limit: 10,
|
||||
Offset: 0,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: " LIMIT $1",
|
||||
args: []any{uint32(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
fields: fields{
|
||||
Limit: 0,
|
||||
Offset: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: " OFFSET $1",
|
||||
args: []any{uint32(10)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both",
|
||||
fields: fields{
|
||||
Limit: 10,
|
||||
Offset: 10,
|
||||
},
|
||||
want: wantQuery{
|
||||
query: " LIMIT $1 OFFSET $2",
|
||||
args: []any{uint32(10), uint32(10)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var stmt Statement
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Pagination{
|
||||
Limit: tt.fields.Limit,
|
||||
Offset: tt.fields.Offset,
|
||||
}
|
||||
p.Write(&stmt)
|
||||
assertQuery(t, &stmt, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
75
apps/api/internal/v2/database/sql_helper.go
Normal file
75
apps/api/internal/v2/database/sql_helper.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
func CloseTx(tx Tx, err error) error {
|
||||
if err != nil {
|
||||
rollbackErr := tx.Rollback()
|
||||
logging.OnError(rollbackErr).Debug("unable to rollback")
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
type DestMapper[R any] func(index int, scan func(dest ...any) error) (*R, error)
|
||||
|
||||
type Rows interface {
|
||||
Close() error
|
||||
Err() error
|
||||
Next() bool
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func MapRows[R any](rows Rows, mapper DestMapper[R]) (result []*R, err error) {
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
logging.OnError(closeErr).Debug("unable to close rows")
|
||||
|
||||
if err == nil && rows.Err() != nil {
|
||||
result = nil
|
||||
err = rows.Err()
|
||||
}
|
||||
}()
|
||||
for i := 0; rows.Next(); i++ {
|
||||
res, err := mapper(i, rows.Scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func MapRowsToObject(rows Rows, mapper func(scan func(dest ...any) error) error) (err error) {
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
logging.OnError(closeErr).Debug("unable to close rows")
|
||||
|
||||
if err == nil && rows.Err() != nil {
|
||||
err = rows.Err()
|
||||
}
|
||||
}()
|
||||
for rows.Next() {
|
||||
err = mapper(rows.Scan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Querier interface {
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
}
|
512
apps/api/internal/v2/database/sql_helper_test.go
Normal file
512
apps/api/internal/v2/database/sql_helper_test.go
Normal file
@@ -0,0 +1,512 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloseTx(t *testing.T) {
|
||||
type args struct {
|
||||
tx *testTx
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
assertErr func(t *testing.T, err error) bool
|
||||
}{
|
||||
{
|
||||
name: "exec err",
|
||||
args: args{
|
||||
tx: &testTx{
|
||||
rollback: execution{
|
||||
shouldExecute: true,
|
||||
},
|
||||
},
|
||||
err: errExec,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("execution error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exec err and rollback err",
|
||||
args: args{
|
||||
tx: &testTx{
|
||||
rollback: execution{
|
||||
err: true,
|
||||
shouldExecute: true,
|
||||
},
|
||||
},
|
||||
err: errExec,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("execution error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "commit Err",
|
||||
args: args{
|
||||
tx: &testTx{
|
||||
commit: execution{
|
||||
err: true,
|
||||
shouldExecute: true,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errCommit)
|
||||
if !is {
|
||||
t.Errorf("commit error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no err",
|
||||
args: args{
|
||||
tx: &testTx{
|
||||
commit: execution{
|
||||
shouldExecute: true,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := err == nil
|
||||
if !is {
|
||||
t.Errorf("no error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := CloseTx(tt.args.tx, tt.args.err)
|
||||
tt.assertErr(t, err)
|
||||
tt.args.tx.assert(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapRows(t *testing.T) {
|
||||
type args struct {
|
||||
rows *testRows
|
||||
mapper DestMapper[string]
|
||||
}
|
||||
var emptyString string
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantResult []*string
|
||||
assertErr func(t *testing.T, err error) bool
|
||||
}{
|
||||
{
|
||||
name: "no rows, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
closeErr: true,
|
||||
},
|
||||
mapper: nil,
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errClose)
|
||||
if !is {
|
||||
t.Errorf("close error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no rows, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
hasErr: true,
|
||||
},
|
||||
mapper: nil,
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errRows)
|
||||
if !is {
|
||||
t.Errorf("rows error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "scan err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
scanErr: true,
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(index int, scan func(dest ...any) error) (*string, error) {
|
||||
var s string
|
||||
if err := scan(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
},
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errScan)
|
||||
if !is {
|
||||
t.Errorf("scan error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exec err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(index int, scan func(dest ...any) error) (*string, error) {
|
||||
return nil, errExec
|
||||
},
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("exec error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exec err, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
closeErr: true,
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(index int, scan func(dest ...any) error) (*string, error) {
|
||||
return nil, errExec
|
||||
},
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("exec error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rows err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
hasErr: true,
|
||||
},
|
||||
mapper: func(index int, scan func(dest ...any) error) (*string, error) {
|
||||
var s string
|
||||
if err := scan(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
},
|
||||
},
|
||||
wantResult: nil,
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errRows)
|
||||
if !is {
|
||||
t.Errorf("rows error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(index int, scan func(dest ...any) error) (*string, error) {
|
||||
var s string
|
||||
if err := scan(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
},
|
||||
},
|
||||
wantResult: []*string{&emptyString},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := err == nil
|
||||
if !is {
|
||||
t.Errorf("no error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotResult, err := MapRows(tt.args.rows, tt.args.mapper)
|
||||
tt.assertErr(t, err)
|
||||
if !reflect.DeepEqual(gotResult, tt.wantResult) {
|
||||
t.Errorf("MapRows() = %v, want %v", gotResult, tt.wantResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapRowsToObject(t *testing.T) {
|
||||
type args struct {
|
||||
rows *testRows
|
||||
mapper func(scan func(dest ...any) error) error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
assertErr func(t *testing.T, err error) bool
|
||||
}{
|
||||
{
|
||||
name: "no rows, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
closeErr: true,
|
||||
},
|
||||
mapper: nil,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errClose)
|
||||
if !is {
|
||||
t.Errorf("close error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no rows, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
hasErr: true,
|
||||
},
|
||||
mapper: nil,
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errRows)
|
||||
if !is {
|
||||
t.Errorf("rows error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "scan err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
scanErr: true,
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(scan func(dest ...any) error) error {
|
||||
var s string
|
||||
if err := scan(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errScan)
|
||||
if !is {
|
||||
t.Errorf("scan error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exec err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(scan func(dest ...any) error) error {
|
||||
return errExec
|
||||
},
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("exec error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exec err, close err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
closeErr: true,
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(scan func(dest ...any) error) error {
|
||||
return errExec
|
||||
},
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errExec)
|
||||
if !is {
|
||||
t.Errorf("exec error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rows err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
hasErr: true,
|
||||
},
|
||||
mapper: func(scan func(dest ...any) error) error {
|
||||
var s string
|
||||
return scan(&s)
|
||||
},
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := errors.Is(err, errRows)
|
||||
if !is {
|
||||
t.Errorf("rows error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no err",
|
||||
args: args{
|
||||
rows: &testRows{
|
||||
nextCount: 1,
|
||||
},
|
||||
mapper: func(scan func(dest ...any) error) error {
|
||||
var s string
|
||||
return scan(&s)
|
||||
},
|
||||
},
|
||||
assertErr: func(t *testing.T, err error) bool {
|
||||
is := err == nil
|
||||
if !is {
|
||||
t.Errorf("no error expected, got: %v", err)
|
||||
}
|
||||
return is
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := MapRowsToObject(tt.args.rows, tt.args.mapper)
|
||||
tt.assertErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var _ Tx = (*testTx)(nil)
|
||||
|
||||
type testTx struct {
|
||||
commit, rollback execution
|
||||
}
|
||||
|
||||
type execution struct {
|
||||
err bool
|
||||
didExecute bool
|
||||
shouldExecute bool
|
||||
}
|
||||
|
||||
var (
|
||||
errCommit = errors.New("commit err")
|
||||
errRollback = errors.New("rollback err")
|
||||
errExec = errors.New("exec err")
|
||||
)
|
||||
|
||||
// Commit implements Tx.
|
||||
func (t *testTx) Commit() error {
|
||||
t.commit.didExecute = true
|
||||
if t.commit.err {
|
||||
return errCommit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback implements Tx.
|
||||
func (t *testTx) Rollback() error {
|
||||
t.rollback.didExecute = true
|
||||
if t.rollback.err {
|
||||
return errRollback
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *testTx) assert(t *testing.T) {
|
||||
if tx.commit.didExecute != tx.commit.shouldExecute {
|
||||
t.Errorf("unexpected execution of commit: should %v, did: %v", tx.commit.shouldExecute, tx.commit.didExecute)
|
||||
}
|
||||
if tx.rollback.didExecute != tx.rollback.shouldExecute {
|
||||
t.Errorf("unexpected execution of rollback: should %v, did: %v", tx.rollback.shouldExecute, tx.rollback.didExecute)
|
||||
}
|
||||
}
|
||||
|
||||
var _ Rows = (*testRows)(nil)
|
||||
|
||||
var (
|
||||
errClose = errors.New("err close")
|
||||
errRows = errors.New("err rows")
|
||||
errScan = errors.New("err scan")
|
||||
)
|
||||
|
||||
type testRows struct {
|
||||
closeErr bool
|
||||
scanErr bool
|
||||
hasErr bool
|
||||
nextCount int
|
||||
}
|
||||
|
||||
// Close implements Rows.
|
||||
func (t *testRows) Close() error {
|
||||
if t.closeErr {
|
||||
return errClose
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Err implements Rows.
|
||||
func (t *testRows) Err() error {
|
||||
if t.hasErr {
|
||||
return errRows
|
||||
}
|
||||
if t.closeErr {
|
||||
return errClose
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next implements Rows.
|
||||
func (t *testRows) Next() bool {
|
||||
t.nextCount--
|
||||
return t.nextCount >= 0
|
||||
}
|
||||
|
||||
// Scan implements Rows.
|
||||
func (t *testRows) Scan(dest ...any) error {
|
||||
if t.scanErr {
|
||||
return errScan
|
||||
}
|
||||
return nil
|
||||
}
|
222
apps/api/internal/v2/database/statement.go
Normal file
222
apps/api/internal/v2/database/statement.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
type Statement struct {
|
||||
addr *Statement
|
||||
builder strings.Builder
|
||||
|
||||
args []any
|
||||
// key is the name of the arg and value is the placeholder
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// namedArgs map[placeholder]string
|
||||
}
|
||||
|
||||
func (stmt *Statement) Args() []any {
|
||||
if stmt == nil {
|
||||
return nil
|
||||
}
|
||||
return stmt.args
|
||||
}
|
||||
|
||||
func (stmt *Statement) Reset() {
|
||||
stmt.builder.Reset()
|
||||
stmt.addr = nil
|
||||
stmt.args = nil
|
||||
}
|
||||
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// SetNamedArg sets the arg and makes it available for query construction
|
||||
// func (stmt *Statement) SetNamedArg(name placeholder, value any) (placeholder string) {
|
||||
// stmt.copyCheck()
|
||||
// stmt.args = append(stmt.args, value)
|
||||
// placeholder = fmt.Sprintf("$%d", len(stmt.args))
|
||||
// if !strings.HasPrefix(name.string, "@") {
|
||||
// name.string = "@" + name.string
|
||||
// }
|
||||
// stmt.namedArgs[name] = placeholder
|
||||
// return placeholder
|
||||
// }
|
||||
|
||||
// AppendArgs appends the args without writing it to Builder
|
||||
// if any arg is a [placeholder] it's replaced with the placeholders parameter
|
||||
func (stmt *Statement) AppendArgs(args ...any) {
|
||||
stmt.copyCheck()
|
||||
stmt.args = slices.Grow(stmt.args, len(args))
|
||||
for _, arg := range args {
|
||||
stmt.AppendArg(arg)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendArg appends the arg without writing it to Builder
|
||||
// if the arg is a [placeholder] it's replaced with the placeholders parameter
|
||||
func (stmt *Statement) AppendArg(arg any) int {
|
||||
stmt.copyCheck()
|
||||
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// if namedArg, ok := arg.(sql.NamedArg); ok {
|
||||
// stmt.SetNamedArg(placeholder{namedArg.Name}, namedArg.Value)
|
||||
// return
|
||||
// }
|
||||
stmt.args = append(stmt.args, arg)
|
||||
return len(stmt.args)
|
||||
}
|
||||
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// func Placeholder(name string) placeholder {
|
||||
// return placeholder{name}
|
||||
// }
|
||||
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// type placeholder struct {
|
||||
// string
|
||||
// }
|
||||
|
||||
// WriteArgs appends the args and adds the placeholders comma separated to [stmt.Builder]
|
||||
// if any arg is a [placeholder] it's replaced with the placeholders parameter
|
||||
func (stmt *Statement) WriteArgs(args ...any) {
|
||||
stmt.copyCheck()
|
||||
stmt.args = slices.Grow(stmt.args, len(args))
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
stmt.WriteString(", ")
|
||||
}
|
||||
stmt.WriteArg(arg)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteArg appends the arg and adds the placeholder to [stmt.Builder]
|
||||
// if the arg is a [placeholder] it's replaced with the placeholders parameter
|
||||
func (stmt *Statement) WriteArg(arg any) {
|
||||
stmt.copyCheck()
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// if namedPlaceholder, ok := arg.(placeholder); ok {
|
||||
// stmt.writeNamedPlaceholder(namedPlaceholder)
|
||||
// return
|
||||
// }
|
||||
placeholder := stmt.AppendArg(arg)
|
||||
stmt.WriteString("$")
|
||||
stmt.WriteString(strconv.Itoa(placeholder))
|
||||
}
|
||||
|
||||
// WriteString extends [strings.Builder.WriteString]
|
||||
// it replaces named args with the previously provided named args
|
||||
func (stmt *Statement) WriteString(s string) {
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// for name, placeholder := range stmt.namedArgs {
|
||||
// s = strings.ReplaceAll(s, name.string, placeholder)
|
||||
// }
|
||||
stmt.builder.WriteString(s)
|
||||
}
|
||||
|
||||
// WriteRune extends [strings.Builder.WriteRune]
|
||||
func (stmt *Statement) WriteRune(r rune) {
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// for name, placeholder := range stmt.namedArgs {
|
||||
// s = strings.ReplaceAll(s, name.string, placeholder)
|
||||
// }
|
||||
stmt.builder.WriteRune(r)
|
||||
}
|
||||
|
||||
// WriteByte extends [strings.Builder.WriteByte]
|
||||
func (stmt *Statement) WriteByte(b byte) {
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// for name, placeholder := range stmt.namedArgs {
|
||||
// s = strings.ReplaceAll(s, name.string, placeholder)
|
||||
// }
|
||||
err := stmt.builder.WriteByte(b)
|
||||
logging.OnError(err).Warn("unable to write bytes")
|
||||
}
|
||||
|
||||
// Write extends [strings.Builder.Write]
|
||||
// it replaces named args with the previously provided named args
|
||||
func (stmt *Statement) Write(b []byte) {
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// for name, placeholder := range stmt.namedArgs {
|
||||
// bytes.ReplaceAll(b, []byte(name.string), []byte(placeholder))
|
||||
// }
|
||||
stmt.builder.Write(b)
|
||||
}
|
||||
|
||||
// String builds the query and replaces placeholders starting with "@"
|
||||
// with the corresponding named arg placeholder
|
||||
func (stmt *Statement) String() string {
|
||||
return stmt.builder.String()
|
||||
}
|
||||
|
||||
// Debug builds the statement and replaces the placeholders with the parameters
|
||||
func (stmt *Statement) Debug() string {
|
||||
query := stmt.String()
|
||||
|
||||
for i := len(stmt.args) - 1; i >= 0; i-- {
|
||||
var argText string
|
||||
switch arg := stmt.args[i].(type) {
|
||||
case time.Time:
|
||||
argText = "'" + arg.Format("2006-01-02 15:04:05Z07:00") + "'"
|
||||
case string:
|
||||
argText = "'" + arg + "'"
|
||||
case []string:
|
||||
argText = "ARRAY["
|
||||
for i, a := range arg {
|
||||
if i > 0 {
|
||||
argText += ", "
|
||||
}
|
||||
argText += "'" + a + "'"
|
||||
}
|
||||
argText += "]"
|
||||
default:
|
||||
argText = fmt.Sprint(arg)
|
||||
}
|
||||
query = strings.ReplaceAll(query, "$"+strconv.Itoa(i+1), argText)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// func (stmt *Statement) writeNamedPlaceholder(arg placeholder) {
|
||||
// placeholder, ok := stmt.namedArgs[arg]
|
||||
// if !ok {
|
||||
// logging.WithFields("named_placeholder", arg).Fatal("named placeholder not defined")
|
||||
// }
|
||||
// stmt.Builder.WriteString(placeholder)
|
||||
// }
|
||||
|
||||
// copyCheck allows uninitialized usage of stmt
|
||||
func (stmt *Statement) copyCheck() {
|
||||
if stmt.addr == nil {
|
||||
// This hack works around a failing of Go's escape analysis
|
||||
// that was causing b to escape and be heap allocated.
|
||||
// See issue 23382.
|
||||
// TODO: once issue 7921 is fixed, this should be reverted to
|
||||
// just "stmt.addr = stmt".
|
||||
stmt.addr = (*Statement)(noescape(unsafe.Pointer(stmt)))
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// stmt.namedArgs = make(map[placeholder]string)
|
||||
} else if stmt.addr != stmt {
|
||||
panic("statement: illegal use of non-zero Builder copied by value")
|
||||
}
|
||||
}
|
||||
|
||||
// noescape hides a pointer from escape analysis. It is the identity function
|
||||
// but escape analysis doesn't think the output depends on the input.
|
||||
// noescape is inlined and currently compiles down to zero instructions.
|
||||
// USE CAREFULLY!
|
||||
// This was copied from the runtime; see issues 23382 and 7921.
|
||||
//
|
||||
//go:nosplit
|
||||
//go:nocheckptr
|
||||
func noescape(p unsafe.Pointer) unsafe.Pointer {
|
||||
x := uintptr(p)
|
||||
//nolint: staticcheck
|
||||
return unsafe.Pointer(x ^ 0)
|
||||
}
|
73
apps/api/internal/v2/database/statement_test.go
Normal file
73
apps/api/internal/v2/database/statement_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStatement_WriteArgs(t *testing.T) {
|
||||
type args struct {
|
||||
args []any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want wantQuery
|
||||
}{
|
||||
{
|
||||
name: "no args",
|
||||
args: args{
|
||||
args: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 arg",
|
||||
args: args{
|
||||
args: []any{"asdf"},
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "$1",
|
||||
args: []any{"asdf"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "n args",
|
||||
args: args{
|
||||
args: []any{"asdf", "jkl", 1},
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "$1, $2, $3",
|
||||
args: []any{"asdf", "jkl", 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var stmt Statement
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stmt.WriteArgs(tt.args.args...)
|
||||
assertQuery(t, &stmt, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type wantQuery struct {
|
||||
query string
|
||||
args []any
|
||||
}
|
||||
|
||||
func assertQuery(t *testing.T, stmt *Statement, want wantQuery) {
|
||||
if want.query != stmt.String() {
|
||||
t.Errorf("unexpected query: want: %q got: %q", want.query, stmt.String())
|
||||
}
|
||||
|
||||
if len(want.args) != len(stmt.Args()) {
|
||||
t.Errorf("unexpected length of args: want %d, got %d", len(want.args), len(stmt.Args()))
|
||||
return
|
||||
}
|
||||
|
||||
for i, wantArg := range want.args {
|
||||
if !reflect.DeepEqual(wantArg, stmt.Args()[i]) {
|
||||
t.Errorf("unexpected arg at position %d: want: %v, got: %v", i, wantArg, stmt.Args()[i])
|
||||
}
|
||||
}
|
||||
}
|
132
apps/api/internal/v2/database/text_filter.go
Normal file
132
apps/api/internal/v2/database/text_filter.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
type TextFilter[T text] struct {
|
||||
Filter[textCompare, T]
|
||||
}
|
||||
|
||||
func NewTextEqual[T text](t T) *TextFilter[T] {
|
||||
return newTextFilter(textEqual, t)
|
||||
}
|
||||
|
||||
func NewTextUnequal[T text](t T) *TextFilter[T] {
|
||||
return newTextFilter(textUnequal, t)
|
||||
}
|
||||
|
||||
func NewTextEqualInsensitive[T text](t T) *TextFilter[string] {
|
||||
return newTextFilter(textEqualInsensitive, strings.ToLower(string(t)))
|
||||
}
|
||||
|
||||
func NewTextUnequalInsensitive[T text](t T) *TextFilter[string] {
|
||||
return newTextFilter(textUnequalInsensitive, strings.ToLower(string(t)))
|
||||
}
|
||||
|
||||
func NewTextStartsWith[T text](t T) *TextFilter[T] {
|
||||
return newTextFilter(textStartsWith, t)
|
||||
}
|
||||
|
||||
func NewTextStartsWithInsensitive[T text](t T) *TextFilter[string] {
|
||||
return newTextFilter(textStartsWithInsensitive, strings.ToLower(string(t)))
|
||||
}
|
||||
|
||||
func NewTextEndsWith[T text](t T) *TextFilter[T] {
|
||||
return newTextFilter(textEndsWith, t)
|
||||
}
|
||||
|
||||
func NewTextEndsWithInsensitive[T text](t T) *TextFilter[string] {
|
||||
return newTextFilter(textEndsWithInsensitive, strings.ToLower(string(t)))
|
||||
}
|
||||
|
||||
func NewTextContains[T text](t T) *TextFilter[T] {
|
||||
return newTextFilter(textContains, t)
|
||||
}
|
||||
|
||||
func NewTextContainsInsensitive[T text](t T) *TextFilter[string] {
|
||||
return newTextFilter(textContainsInsensitive, strings.ToLower(string(t)))
|
||||
}
|
||||
|
||||
func newTextFilter[T text](comp textCompare, t T) *TextFilter[T] {
|
||||
return &TextFilter[T]{
|
||||
Filter: Filter[textCompare, T]{
|
||||
comp: comp,
|
||||
value: t,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TextFilter[T]) Write(stmt *Statement, columnName string) {
|
||||
if f.comp.isInsensitive() {
|
||||
f.writeCaseInsensitive(stmt, columnName)
|
||||
return
|
||||
}
|
||||
f.Filter.Write(stmt, columnName)
|
||||
}
|
||||
|
||||
func (f *TextFilter[T]) writeCaseInsensitive(stmt *Statement, columnName string) {
|
||||
stmt.WriteString("LOWER(")
|
||||
stmt.WriteString(columnName)
|
||||
stmt.WriteString(") ")
|
||||
stmt.WriteString(f.comp.String())
|
||||
stmt.WriteRune(' ')
|
||||
f.writeArg(stmt)
|
||||
}
|
||||
|
||||
func (f *TextFilter[T]) writeArg(stmt *Statement) {
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// var v any = f.value
|
||||
// workaround for placeholder
|
||||
// if placeholder, ok := v.(placeholder); ok {
|
||||
// stmt.Builder.WriteString(" LOWER(")
|
||||
// stmt.WriteArg(placeholder)
|
||||
// stmt.Builder.WriteString(")")
|
||||
// }
|
||||
stmt.WriteArg(strings.ToLower(fmt.Sprint(f.value)))
|
||||
}
|
||||
|
||||
type textCompare uint8
|
||||
|
||||
const (
|
||||
textEqual textCompare = iota
|
||||
textUnequal
|
||||
textEqualInsensitive
|
||||
textUnequalInsensitive
|
||||
textStartsWith
|
||||
textStartsWithInsensitive
|
||||
textEndsWith
|
||||
textEndsWithInsensitive
|
||||
textContains
|
||||
textContainsInsensitive
|
||||
)
|
||||
|
||||
func (c textCompare) String() string {
|
||||
switch c {
|
||||
case textEqual, textEqualInsensitive:
|
||||
return "="
|
||||
case textUnequal, textUnequalInsensitive:
|
||||
return "<>"
|
||||
case textStartsWith, textStartsWithInsensitive, textEndsWith, textEndsWithInsensitive, textContains, textContainsInsensitive:
|
||||
return "LIKE"
|
||||
default:
|
||||
logging.WithFields("compare", c).Panic("comparison type not implemented")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c textCompare) isInsensitive() bool {
|
||||
return c == textEqualInsensitive ||
|
||||
c == textStartsWithInsensitive ||
|
||||
c == textEndsWithInsensitive ||
|
||||
c == textContainsInsensitive
|
||||
}
|
||||
|
||||
type text interface {
|
||||
~string
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// ~string | placeholder
|
||||
}
|
351
apps/api/internal/v2/database/text_filter_test.go
Normal file
351
apps/api/internal/v2/database/text_filter_test.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTextEqual(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t string) *TextFilter[string]
|
||||
t string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *TextFilter[string]
|
||||
}{
|
||||
{
|
||||
name: "NewTextEqual",
|
||||
args: args{
|
||||
constructor: NewTextEqual[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEqual,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextUnequal",
|
||||
args: args{
|
||||
constructor: NewTextUnequal[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textUnequal,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEqualInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextEqualInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEqualInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEqualInsensitive check lower",
|
||||
args: args{
|
||||
constructor: NewTextEqualInsensitive[string],
|
||||
t: "tEXt",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEqualInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextUnequalInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextUnequalInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textUnequalInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextUnequalInsensitive check lower",
|
||||
args: args{
|
||||
constructor: NewTextUnequalInsensitive[string],
|
||||
t: "tEXt",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textUnequalInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextStartsWith",
|
||||
args: args{
|
||||
constructor: NewTextStartsWith[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textStartsWith,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextStartsWithInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextStartsWithInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textStartsWithInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextStartsWithInsensitive check lower",
|
||||
args: args{
|
||||
constructor: NewTextStartsWithInsensitive[string],
|
||||
t: "tEXt",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textStartsWithInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEndsWith",
|
||||
args: args{
|
||||
constructor: NewTextEndsWith[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEndsWith,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEndsWithInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextEndsWithInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEndsWithInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEndsWithInsensitive check lower",
|
||||
args: args{
|
||||
constructor: NewTextEndsWithInsensitive[string],
|
||||
t: "tEXt",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textEndsWithInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextContains",
|
||||
args: args{
|
||||
constructor: NewTextContains[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textContains,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextContainsInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextContainsInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textContainsInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextContainsInsensitive to lower",
|
||||
args: args{
|
||||
constructor: NewTextContainsInsensitive[string],
|
||||
t: "tEXt",
|
||||
},
|
||||
want: &TextFilter[string]{
|
||||
Filter: Filter[textCompare, string]{
|
||||
comp: textContainsInsensitive,
|
||||
value: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.args.constructor(tt.args.t); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NewTextEqual() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextConditionWrite(t *testing.T) {
|
||||
type args struct {
|
||||
constructor func(t string) *TextFilter[string]
|
||||
t string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want wantQuery
|
||||
}{
|
||||
{
|
||||
name: "NewTextEqual",
|
||||
args: args{
|
||||
constructor: NewTextEqual[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test = $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextUnequal",
|
||||
args: args{
|
||||
constructor: NewTextUnequal[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test <> $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEqualInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextEqualInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "LOWER(test) = $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextUnequalInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextUnequalInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test <> $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextStartsWith",
|
||||
args: args{
|
||||
constructor: NewTextStartsWith[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextStartsWithInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextStartsWithInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "LOWER(test) LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEndsWith",
|
||||
args: args{
|
||||
constructor: NewTextEndsWith[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextEndsWithInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextEndsWithInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "LOWER(test) LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextContains",
|
||||
args: args{
|
||||
constructor: NewTextContains[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "test LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NewTextContainsInsensitive",
|
||||
args: args{
|
||||
constructor: NewTextContainsInsensitive[string],
|
||||
t: "text",
|
||||
},
|
||||
want: wantQuery{
|
||||
query: "LOWER(test) LIKE $1",
|
||||
args: []any{"text"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var stmt Statement
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.constructor(tt.args.t).Write(&stmt, "test")
|
||||
assertQuery(t, &stmt, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
7
apps/api/internal/v2/domain/added.go
Normal file
7
apps/api/internal/v2/domain/added.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
const AddedTypeSuffix = "domain.added"
|
||||
|
||||
type AddedPayload struct {
|
||||
Name string `json:"domain"`
|
||||
}
|
7
apps/api/internal/v2/domain/primary_set.go
Normal file
7
apps/api/internal/v2/domain/primary_set.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
const PrimarySetTypeSuffix = "domain.primary.set"
|
||||
|
||||
type PrimarySetPayload struct {
|
||||
Name string `json:"domain"`
|
||||
}
|
7
apps/api/internal/v2/domain/removed.go
Normal file
7
apps/api/internal/v2/domain/removed.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
const RemovedTypeSuffix = "domain.removed"
|
||||
|
||||
type RemovedPayload struct {
|
||||
Name string `json:"domain"`
|
||||
}
|
7
apps/api/internal/v2/domain/verfied.go
Normal file
7
apps/api/internal/v2/domain/verfied.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
const VerifiedTypeSuffix = "domain.verified"
|
||||
|
||||
type VerifiedPayload struct {
|
||||
Name string `json:"domain"`
|
||||
}
|
24
apps/api/internal/v2/eventstore/aggregate.go
Normal file
24
apps/api/internal/v2/eventstore/aggregate.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package eventstore
|
||||
|
||||
type Aggregate struct {
|
||||
ID string
|
||||
Type string
|
||||
Instance string
|
||||
Owner string
|
||||
}
|
||||
|
||||
func (agg *Aggregate) Equals(aggregate *Aggregate) bool {
|
||||
if aggregate.ID != "" && aggregate.ID != agg.ID {
|
||||
return false
|
||||
}
|
||||
if aggregate.Type != "" && aggregate.Type != agg.Type {
|
||||
return false
|
||||
}
|
||||
if aggregate.Instance != "" && aggregate.Instance != agg.Instance {
|
||||
return false
|
||||
}
|
||||
if aggregate.Owner != "" && aggregate.Owner != agg.Owner {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
29
apps/api/internal/v2/eventstore/current_sequence.go
Normal file
29
apps/api/internal/v2/eventstore/current_sequence.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package eventstore
|
||||
|
||||
type CurrentSequence func(current uint32) bool
|
||||
|
||||
func CheckSequence(current uint32, check CurrentSequence) bool {
|
||||
if check == nil {
|
||||
return true
|
||||
}
|
||||
return check(current)
|
||||
}
|
||||
|
||||
// SequenceIgnore doesn't check the current sequence
|
||||
func SequenceIgnore() CurrentSequence {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SequenceMatches exactly the provided sequence
|
||||
func SequenceMatches(sequence uint32) CurrentSequence {
|
||||
return func(current uint32) bool {
|
||||
return current == sequence
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceAtLeast matches the given sequence <= the current sequence
|
||||
func SequenceAtLeast(sequence uint32) CurrentSequence {
|
||||
return func(current uint32) bool {
|
||||
return current >= sequence
|
||||
}
|
||||
}
|
66
apps/api/internal/v2/eventstore/event.go
Normal file
66
apps/api/internal/v2/eventstore/event.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Unmarshal func(ptr any) error
|
||||
|
||||
type Payload interface {
|
||||
Unmarshal | any
|
||||
}
|
||||
|
||||
type Action[P Payload] struct {
|
||||
Creator string
|
||||
Type string
|
||||
Revision uint16
|
||||
Payload P
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Action[any]
|
||||
UniqueConstraints []*UniqueConstraint
|
||||
}
|
||||
|
||||
type StorageEvent struct {
|
||||
Action[Unmarshal]
|
||||
|
||||
Aggregate Aggregate
|
||||
CreatedAt time.Time
|
||||
Position GlobalPosition
|
||||
Sequence uint32
|
||||
}
|
||||
|
||||
type Event[P any] struct {
|
||||
*StorageEvent
|
||||
Payload P
|
||||
}
|
||||
|
||||
func UnmarshalPayload[P any](unmarshal Unmarshal) (P, error) {
|
||||
var payload P
|
||||
err := unmarshal(&payload)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
type EmptyPayload struct{}
|
||||
|
||||
type TypeChecker interface {
|
||||
ActionType() string
|
||||
}
|
||||
|
||||
func Type[T TypeChecker]() string {
|
||||
var t T
|
||||
return t.ActionType()
|
||||
}
|
||||
|
||||
func IsType[T TypeChecker](types ...string) bool {
|
||||
gotten := Type[T]()
|
||||
|
||||
for _, typ := range types {
|
||||
if gotten == typ {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
47
apps/api/internal/v2/eventstore/event_store.go
Normal file
47
apps/api/internal/v2/eventstore/event_store.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func NewEventstore(querier Querier, pusher Pusher) *EventStore {
|
||||
return &EventStore{
|
||||
Pusher: pusher,
|
||||
Querier: querier,
|
||||
}
|
||||
}
|
||||
|
||||
func NewEventstoreFromOne(o one) *EventStore {
|
||||
return NewEventstore(o, o)
|
||||
}
|
||||
|
||||
type EventStore struct {
|
||||
Pusher
|
||||
Querier
|
||||
}
|
||||
|
||||
type one interface {
|
||||
Pusher
|
||||
Querier
|
||||
}
|
||||
|
||||
type healthier interface {
|
||||
Health(ctx context.Context) error
|
||||
}
|
||||
|
||||
type GlobalPosition struct {
|
||||
Position decimal.Decimal
|
||||
InPositionOrder uint32
|
||||
}
|
||||
|
||||
func (gp GlobalPosition) IsLess(other GlobalPosition) bool {
|
||||
return gp.Position.LessThan(other.Position) || (gp.Position.Equal(other.Position) && gp.InPositionOrder < other.InPositionOrder)
|
||||
}
|
||||
|
||||
type Reducer interface {
|
||||
Reduce(events ...*StorageEvent) error
|
||||
}
|
||||
|
||||
type Reduce func(events ...*StorageEvent) error
|
64
apps/api/internal/v2/eventstore/postgres/event.go
Normal file
64
apps/api/internal/v2/eventstore/postgres/event.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func intentToCommands(intent *intent) (commands []*command, err error) {
|
||||
commands = make([]*command, len(intent.Commands()))
|
||||
|
||||
for i, cmd := range intent.Commands() {
|
||||
payload, err := marshalPayload(cmd.Payload)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "POSTG-MInPK", "Errors.Internal")
|
||||
}
|
||||
commands[i] = &command{
|
||||
Command: cmd,
|
||||
intent: intent,
|
||||
sequence: intent.nextSequence(),
|
||||
payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func marshalPayload(payload any) ([]byte, error) {
|
||||
if payload == nil || reflect.ValueOf(payload).IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
type command struct {
|
||||
*eventstore.Command
|
||||
|
||||
intent *intent
|
||||
|
||||
payload []byte
|
||||
position eventstore.GlobalPosition
|
||||
createdAt time.Time
|
||||
sequence uint32
|
||||
}
|
||||
|
||||
func (cmd *command) toEvent() *eventstore.StorageEvent {
|
||||
return &eventstore.StorageEvent{
|
||||
Action: eventstore.Action[eventstore.Unmarshal]{
|
||||
Creator: cmd.Creator,
|
||||
Type: cmd.Type,
|
||||
Revision: cmd.Revision,
|
||||
Payload: func(ptr any) error {
|
||||
return json.Unmarshal(cmd.payload, ptr)
|
||||
},
|
||||
},
|
||||
Aggregate: *cmd.intent.Aggregate(),
|
||||
Sequence: cmd.intent.sequence,
|
||||
Position: cmd.position,
|
||||
CreatedAt: cmd.createdAt,
|
||||
}
|
||||
}
|
47
apps/api/internal/v2/eventstore/postgres/intent.go
Normal file
47
apps/api/internal/v2/eventstore/postgres/intent.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
type intent struct {
|
||||
*eventstore.PushAggregate
|
||||
|
||||
sequence uint32
|
||||
}
|
||||
|
||||
func (i *intent) nextSequence() uint32 {
|
||||
i.sequence++
|
||||
return i.sequence
|
||||
}
|
||||
|
||||
func makeIntents(pushIntent *eventstore.PushIntent) []*intent {
|
||||
res := make([]*intent, len(pushIntent.Aggregates()))
|
||||
|
||||
for i, aggregate := range pushIntent.Aggregates() {
|
||||
res[i] = &intent{PushAggregate: aggregate}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func intentByAggregate(intents []*intent, aggregate *eventstore.Aggregate) *intent {
|
||||
for _, intent := range intents {
|
||||
if intent.PushAggregate.Aggregate().Equals(aggregate) {
|
||||
return intent
|
||||
}
|
||||
}
|
||||
logging.WithFields("instance", aggregate.Instance, "owner", aggregate.Owner, "type", aggregate.Type, "id", aggregate.ID).Panic("no intent found")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSequences(intents []*intent) bool {
|
||||
for _, intent := range intents {
|
||||
if !eventstore.CheckSequence(intent.sequence, intent.PushAggregate.CurrentSequence()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
122
apps/api/internal/v2/eventstore/postgres/intent_test.go
Normal file
122
apps/api/internal/v2/eventstore/postgres/intent_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
func Test_checkSequences(t *testing.T) {
|
||||
type args struct {
|
||||
intents []*intent
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "ignore",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 1,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.IgnoreCurrentSequence(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ignores",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 1,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.IgnoreCurrentSequence(),
|
||||
),
|
||||
},
|
||||
{
|
||||
sequence: 1,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 0,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.CurrentSequenceMatches(0),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 1,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.CurrentSequenceMatches(2),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "at least",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 10,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.CurrentSequenceAtLeast(0),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "at least too low",
|
||||
args: args{
|
||||
intents: []*intent{
|
||||
{
|
||||
sequence: 1,
|
||||
PushAggregate: eventstore.NewPushAggregate(
|
||||
"", "", "",
|
||||
eventstore.CurrentSequenceAtLeast(2),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := checkSequences(tt.args.intents); got != tt.want {
|
||||
t.Errorf("checkSequences() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
262
apps/api/internal/v2/eventstore/postgres/push.go
Normal file
262
apps/api/internal/v2/eventstore/postgres/push.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/v2/crdb"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/v2/database"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// Push implements eventstore.Pusher.
|
||||
func (s *Storage) Push(ctx context.Context, intent *eventstore.PushIntent) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
tx := intent.Tx()
|
||||
if tx == nil {
|
||||
tx, err = s.client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: false})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = database.CloseTx(tx, err)
|
||||
}()
|
||||
}
|
||||
|
||||
var retryCount uint32
|
||||
return crdb.Execute(func() (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retryCount < s.config.MaxRetries {
|
||||
retryCount++
|
||||
return
|
||||
}
|
||||
logging.WithFields("retry_count", retryCount).WithError(err).Debug("max retry count reached")
|
||||
err = zerrors.ThrowInternal(err, "POSTG-VJfJz", "Errors.Internal")
|
||||
}()
|
||||
// allows smaller wait times on query side for instances which are not actively writing
|
||||
if err := setAppName(ctx, tx, "es_pusher_"+intent.Instance()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intents, err := lockAggregates(ctx, tx, intent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !checkSequences(intents) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POSTG-KOM6E", "Errors.Internal.Eventstore.SequenceNotMatched")
|
||||
}
|
||||
|
||||
commands := make([]*command, 0, len(intents))
|
||||
for _, intent := range intents {
|
||||
additionalCommands, err := intentToCommands(intent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands = append(commands, additionalCommands...)
|
||||
}
|
||||
|
||||
err = uniqueConstraints(ctx, tx, commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.push(ctx, tx, intent, commands)
|
||||
})
|
||||
}
|
||||
|
||||
// setAppName for the the current transaction
|
||||
func setAppName(ctx context.Context, tx *sql.Tx, name string) error {
|
||||
_, err := tx.ExecContext(ctx, fmt.Sprintf("SET LOCAL application_name TO '%s'", name))
|
||||
if err != nil {
|
||||
logging.WithFields("name", name).WithError(err).Debug("setting app name failed")
|
||||
return zerrors.ThrowInternal(err, "POSTG-G3OmZ", "Errors.Internal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lockAggregates(ctx context.Context, tx *sql.Tx, intent *eventstore.PushIntent) (_ []*intent, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var stmt database.Statement
|
||||
|
||||
stmt.WriteString("WITH existing AS (")
|
||||
for i, aggregate := range intent.Aggregates() {
|
||||
if i > 0 {
|
||||
stmt.WriteString(" UNION ALL ")
|
||||
}
|
||||
stmt.WriteString(`(SELECT instance_id, aggregate_type, aggregate_id, "sequence" FROM eventstore.events2 WHERE instance_id = `)
|
||||
stmt.WriteArgs(intent.Instance())
|
||||
stmt.WriteString(` AND aggregate_type = `)
|
||||
stmt.WriteArgs(aggregate.Type())
|
||||
stmt.WriteString(` AND aggregate_id = `)
|
||||
stmt.WriteArgs(aggregate.ID())
|
||||
stmt.WriteString(` AND owner = `)
|
||||
stmt.WriteArgs(aggregate.Owner())
|
||||
stmt.WriteString(` ORDER BY "sequence" DESC LIMIT 1)`)
|
||||
}
|
||||
stmt.WriteString(") SELECT e.instance_id, e.owner, e.aggregate_type, e.aggregate_id, e.sequence FROM eventstore.events2 e JOIN existing ON e.instance_id = existing.instance_id AND e.aggregate_type = existing.aggregate_type AND e.aggregate_id = existing.aggregate_id AND e.sequence = existing.sequence FOR UPDATE")
|
||||
|
||||
//nolint:rowserrcheck
|
||||
// rows is checked by database.MapRowsToObject
|
||||
rows, err := tx.QueryContext(ctx, stmt.String(), stmt.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := makeIntents(intent)
|
||||
|
||||
err = database.MapRowsToObject(rows, func(scan func(dest ...any) error) error {
|
||||
var sequence sql.Null[uint32]
|
||||
agg := new(eventstore.Aggregate)
|
||||
|
||||
err := scan(
|
||||
&agg.Instance,
|
||||
&agg.Owner,
|
||||
&agg.Type,
|
||||
&agg.ID,
|
||||
&sequence,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intentByAggregate(res, agg).sequence = sequence.V
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Storage) push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reducer, commands []*command) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var stmt database.Statement
|
||||
|
||||
stmt.WriteString(`INSERT INTO eventstore.events2 (instance_id, "owner", aggregate_type, aggregate_id, revision, creator, event_type, payload, "sequence", in_tx_order, created_at, "position") VALUES `)
|
||||
for i, cmd := range commands {
|
||||
if i > 0 {
|
||||
stmt.WriteString(", ")
|
||||
}
|
||||
|
||||
cmd.position.InPositionOrder = uint32(i)
|
||||
stmt.WriteString(`(`)
|
||||
stmt.WriteArgs(
|
||||
cmd.intent.Aggregate().Instance,
|
||||
cmd.intent.Aggregate().Owner,
|
||||
cmd.intent.Aggregate().Type,
|
||||
cmd.intent.Aggregate().ID,
|
||||
cmd.Revision,
|
||||
cmd.Creator,
|
||||
cmd.Type,
|
||||
cmd.payload,
|
||||
cmd.sequence,
|
||||
cmd.position.InPositionOrder,
|
||||
)
|
||||
|
||||
stmt.WriteString(", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()))")
|
||||
}
|
||||
stmt.WriteString(` RETURNING created_at, "position"`)
|
||||
|
||||
//nolint:rowserrcheck
|
||||
// rows is checked by database.MapRowsToObject
|
||||
rows, err := tx.QueryContext(ctx, stmt.String(), stmt.Args()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var i int
|
||||
return database.MapRowsToObject(rows, func(scan func(dest ...any) error) error {
|
||||
defer func() { i++ }()
|
||||
|
||||
err := scan(
|
||||
&commands[i].createdAt,
|
||||
&commands[i].position.Position,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return reducer.Reduce(commands[i].toEvent())
|
||||
})
|
||||
}
|
||||
|
||||
func uniqueConstraints(ctx context.Context, tx *sql.Tx, commands []*command) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var stmt database.Statement
|
||||
|
||||
for _, cmd := range commands {
|
||||
if len(cmd.UniqueConstraints) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, constraint := range cmd.UniqueConstraints {
|
||||
stmt.Reset()
|
||||
|
||||
instance := cmd.intent.PushAggregate.Aggregate().Instance
|
||||
if constraint.IsGlobal {
|
||||
instance = ""
|
||||
}
|
||||
switch constraint.Action {
|
||||
case eventstore.UniqueConstraintAdd:
|
||||
stmt.WriteString(`INSERT INTO eventstore.unique_constraints (instance_id, unique_type, unique_field) VALUES (`)
|
||||
stmt.WriteArgs(instance, constraint.UniqueType, constraint.UniqueField)
|
||||
stmt.WriteRune(')')
|
||||
case eventstore.UniqueConstraintInstanceRemove:
|
||||
stmt.WriteString(`DELETE FROM eventstore.unique_constraints WHERE instance_id = `)
|
||||
stmt.WriteArgs(instance)
|
||||
case eventstore.UniqueConstraintRemove:
|
||||
stmt.WriteString(`DELETE FROM eventstore.unique_constraints WHERE `)
|
||||
stmt.WriteString(deleteUniqueConstraintClause)
|
||||
stmt.AppendArgs(
|
||||
instance,
|
||||
constraint.UniqueType,
|
||||
constraint.UniqueField,
|
||||
)
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, stmt.String(), stmt.Args()...)
|
||||
if err != nil {
|
||||
logging.WithFields("action", constraint.Action).Warn("handling of unique constraint failed")
|
||||
errMessage := constraint.ErrorMessage
|
||||
if errMessage == "" {
|
||||
errMessage = "Errors.Internal"
|
||||
}
|
||||
return zerrors.ThrowAlreadyExists(err, "POSTG-QzjyP", errMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// the query is so complex because we accidentally stored unique constraint case sensitive
|
||||
// the query checks first if there is a case sensitive match and afterwards if there is a case insensitive match
|
||||
var deleteUniqueConstraintClause = `
|
||||
(instance_id = $1 AND unique_type = $2 AND unique_field = (
|
||||
SELECT unique_field from (
|
||||
SELECT instance_id, unique_type, unique_field
|
||||
FROM eventstore.unique_constraints
|
||||
WHERE instance_id = $1 AND unique_type = $2 AND unique_field = $3
|
||||
UNION ALL
|
||||
SELECT instance_id, unique_type, unique_field
|
||||
FROM eventstore.unique_constraints
|
||||
WHERE instance_id = $1 AND unique_type = $2 AND unique_field = LOWER($3)
|
||||
) AS case_insensitive_constraints LIMIT 1)
|
||||
)`
|
1318
apps/api/internal/v2/eventstore/postgres/push_test.go
Normal file
1318
apps/api/internal/v2/eventstore/postgres/push_test.go
Normal file
File diff suppressed because it is too large
Load Diff
296
apps/api/internal/v2/eventstore/postgres/query.go
Normal file
296
apps/api/internal/v2/eventstore/postgres/query.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/v2/database"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
func (s *Storage) Query(ctx context.Context, query *eventstore.Query) (eventCount int, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var stmt database.Statement
|
||||
writeQuery(&stmt, query)
|
||||
|
||||
if query.Tx() != nil {
|
||||
return executeQuery(ctx, query.Tx(), &stmt, query)
|
||||
}
|
||||
|
||||
return executeQuery(ctx, s.client.DB, &stmt, query)
|
||||
}
|
||||
|
||||
func executeQuery(ctx context.Context, tx database.Querier, stmt *database.Statement, reducer eventstore.Reducer) (eventCount int, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
//nolint:rowserrcheck
|
||||
// rows is checked by database.MapRowsToObject
|
||||
rows, err := tx.QueryContext(ctx, stmt.String(), stmt.Args()...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = database.MapRowsToObject(rows, func(scan func(dest ...any) error) error {
|
||||
e := new(eventstore.StorageEvent)
|
||||
|
||||
var payload sql.Null[[]byte]
|
||||
|
||||
err := scan(
|
||||
&e.CreatedAt,
|
||||
&e.Type,
|
||||
&e.Sequence,
|
||||
&e.Position.Position,
|
||||
&e.Position.InPositionOrder,
|
||||
&payload,
|
||||
&e.Creator,
|
||||
&e.Aggregate.Owner,
|
||||
&e.Aggregate.Instance,
|
||||
&e.Aggregate.Type,
|
||||
&e.Aggregate.ID,
|
||||
&e.Revision,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Payload = func(ptr any) error {
|
||||
if len(payload.V) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(payload.V, ptr)
|
||||
}
|
||||
eventCount++
|
||||
|
||||
return reducer.Reduce(e)
|
||||
})
|
||||
|
||||
return eventCount, err
|
||||
}
|
||||
|
||||
var (
|
||||
selectColumns = `SELECT created_at, event_type, "sequence", "position", in_tx_order, payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision`
|
||||
// TODO: condition must know if it's args are named parameters or not
|
||||
// instancePlaceholder = database.Placeholder("@instance_id")
|
||||
)
|
||||
|
||||
func writeQuery(stmt *database.Statement, query *eventstore.Query) {
|
||||
stmt.WriteString(selectColumns)
|
||||
// stmt.SetNamedArg(instancePlaceholder, query.Instance())
|
||||
|
||||
stmt.WriteString(" FROM (")
|
||||
writeFilters(stmt, query.Filters())
|
||||
stmt.WriteString(") sub")
|
||||
writePagination(stmt, query.Pagination())
|
||||
}
|
||||
|
||||
var from = " FROM eventstore.events2"
|
||||
|
||||
func writeFilters(stmt *database.Statement, filters []*eventstore.Filter) {
|
||||
if len(filters) == 0 {
|
||||
logging.Fatal("query does not contain filters")
|
||||
}
|
||||
|
||||
for i, filter := range filters {
|
||||
if i > 0 {
|
||||
stmt.WriteString(" UNION ALL ")
|
||||
}
|
||||
stmt.WriteRune('(')
|
||||
stmt.WriteString(selectColumns)
|
||||
stmt.WriteString(from)
|
||||
|
||||
writeFilter(stmt, filter)
|
||||
|
||||
stmt.WriteString(")")
|
||||
}
|
||||
}
|
||||
|
||||
func writeFilter(stmt *database.Statement, filter *eventstore.Filter) {
|
||||
stmt.WriteString(" WHERE ")
|
||||
filter.Parent().Instance().Write(stmt, "instance_id")
|
||||
|
||||
writeAggregateFilters(stmt, filter.AggregateFilters())
|
||||
writePagination(stmt, filter.Pagination())
|
||||
}
|
||||
|
||||
func writePagination(stmt *database.Statement, pagination *eventstore.Pagination) {
|
||||
writePosition(stmt, pagination.Position())
|
||||
writeOrdering(stmt, pagination.Desc())
|
||||
if pagination.Pagination() != nil {
|
||||
pagination.Pagination().Write(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func writePosition(stmt *database.Statement, position *eventstore.PositionCondition) {
|
||||
if position == nil {
|
||||
return
|
||||
}
|
||||
|
||||
max := position.Max()
|
||||
min := position.Min()
|
||||
|
||||
stmt.WriteString(" AND ")
|
||||
|
||||
if max != nil {
|
||||
if max.InPositionOrder > 0 {
|
||||
stmt.WriteString("((")
|
||||
database.NewNumberEquals(max.Position).Write(stmt, "position")
|
||||
stmt.WriteString(" AND ")
|
||||
database.NewNumberLess(max.InPositionOrder).Write(stmt, "in_tx_order")
|
||||
stmt.WriteRune(')')
|
||||
stmt.WriteString(" OR ")
|
||||
}
|
||||
database.NewNumberLess(max.Position).Write(stmt, "position")
|
||||
if max.InPositionOrder > 0 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
if max != nil && min != nil {
|
||||
stmt.WriteString(" AND ")
|
||||
}
|
||||
|
||||
if min != nil {
|
||||
if min.InPositionOrder > 0 {
|
||||
stmt.WriteString("((")
|
||||
database.NewNumberEquals(min.Position).Write(stmt, "position")
|
||||
stmt.WriteString(" AND ")
|
||||
database.NewNumberGreater(min.InPositionOrder).Write(stmt, "in_tx_order")
|
||||
stmt.WriteRune(')')
|
||||
stmt.WriteString(" OR ")
|
||||
}
|
||||
database.NewNumberGreater(min.Position).Write(stmt, "position")
|
||||
if min.InPositionOrder > 0 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeAggregateFilters(stmt *database.Statement, filters []*eventstore.AggregateFilter) {
|
||||
if len(filters) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stmt.WriteString(" AND ")
|
||||
if len(filters) > 1 {
|
||||
stmt.WriteRune('(')
|
||||
}
|
||||
for i, filter := range filters {
|
||||
if i > 0 {
|
||||
stmt.WriteString(" OR ")
|
||||
}
|
||||
writeAggregateFilter(stmt, filter)
|
||||
}
|
||||
if len(filters) > 1 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
func writeAggregateFilter(stmt *database.Statement, filter *eventstore.AggregateFilter) {
|
||||
conditions := definedConditions([]*condition{
|
||||
{column: "owner", condition: filter.Owners()},
|
||||
{column: "aggregate_type", condition: filter.Type()},
|
||||
{column: "aggregate_id", condition: filter.IDs()},
|
||||
})
|
||||
|
||||
if len(conditions) > 1 || len(filter.Events()) > 0 {
|
||||
stmt.WriteRune('(')
|
||||
}
|
||||
|
||||
writeConditions(
|
||||
stmt,
|
||||
conditions,
|
||||
" AND ",
|
||||
)
|
||||
writeEventFilters(stmt, filter.Events())
|
||||
|
||||
if len(conditions) > 1 || len(filter.Events()) > 0 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
func writeEventFilters(stmt *database.Statement, filters []*eventstore.EventFilter) {
|
||||
if len(filters) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stmt.WriteString(" AND ")
|
||||
if len(filters) > 1 {
|
||||
stmt.WriteRune('(')
|
||||
}
|
||||
|
||||
for i, filter := range filters {
|
||||
if i > 0 {
|
||||
stmt.WriteString(" OR ")
|
||||
}
|
||||
writeEventFilter(stmt, filter)
|
||||
}
|
||||
|
||||
if len(filters) > 1 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
func writeEventFilter(stmt *database.Statement, filter *eventstore.EventFilter) {
|
||||
conditions := definedConditions([]*condition{
|
||||
{column: "event_type", condition: filter.Types()},
|
||||
{column: "created_at", condition: filter.CreatedAt()},
|
||||
{column: "sequence", condition: filter.Sequence()},
|
||||
{column: "revision", condition: filter.Revision()},
|
||||
{column: "creator", condition: filter.Creators()},
|
||||
})
|
||||
|
||||
if len(conditions) > 1 {
|
||||
stmt.WriteRune('(')
|
||||
}
|
||||
|
||||
writeConditions(
|
||||
stmt,
|
||||
conditions,
|
||||
" AND ",
|
||||
)
|
||||
|
||||
if len(conditions) > 1 {
|
||||
stmt.WriteRune(')')
|
||||
}
|
||||
}
|
||||
|
||||
type condition struct {
|
||||
column string
|
||||
condition database.Condition
|
||||
}
|
||||
|
||||
func writeConditions(stmt *database.Statement, conditions []*condition, sep string) {
|
||||
var i int
|
||||
for _, cond := range conditions {
|
||||
if i > 0 {
|
||||
stmt.WriteString(sep)
|
||||
}
|
||||
cond.condition.Write(stmt, cond.column)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func definedConditions(conditions []*condition) []*condition {
|
||||
return slices.DeleteFunc(conditions, func(cond *condition) bool {
|
||||
return cond.condition == nil
|
||||
})
|
||||
}
|
||||
|
||||
func writeOrdering(stmt *database.Statement, descending bool) {
|
||||
stmt.WriteString(" ORDER BY position")
|
||||
if descending {
|
||||
stmt.WriteString(" DESC")
|
||||
}
|
||||
|
||||
stmt.WriteString(", in_tx_order")
|
||||
if descending {
|
||||
stmt.WriteString(" DESC")
|
||||
}
|
||||
}
|
1382
apps/api/internal/v2/eventstore/postgres/query_test.go
Normal file
1382
apps/api/internal/v2/eventstore/postgres/query_test.go
Normal file
File diff suppressed because it is too large
Load Diff
34
apps/api/internal/v2/eventstore/postgres/storage.go
Normal file
34
apps/api/internal/v2/eventstore/postgres/storage.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
_ eventstore.Pusher = (*Storage)(nil)
|
||||
_ eventstore.Querier = (*Storage)(nil)
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
client *database.DB
|
||||
config *Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
MaxRetries uint32
|
||||
}
|
||||
|
||||
func New(client *database.DB, config *Config) *Storage {
|
||||
return &Storage{
|
||||
client: client,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Health implements eventstore.Pusher.
|
||||
func (s *Storage) Health(ctx context.Context) error {
|
||||
return s.client.PingContext(ctx)
|
||||
}
|
172
apps/api/internal/v2/eventstore/push.go
Normal file
172
apps/api/internal/v2/eventstore/push.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Pusher interface {
|
||||
healthier
|
||||
// Push writes the intents to the storage
|
||||
// if an intent implements [PushReducerIntent] [PushReducerIntent.Reduce] is called after
|
||||
// the intent was stored
|
||||
Push(ctx context.Context, intent *PushIntent) error
|
||||
}
|
||||
|
||||
func NewPushIntent(instance string, opts ...PushOpt) *PushIntent {
|
||||
intent := &PushIntent{
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(intent)
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
type PushIntent struct {
|
||||
instance string
|
||||
reducer Reducer
|
||||
tx *sql.Tx
|
||||
aggregates []*PushAggregate
|
||||
}
|
||||
|
||||
func (pi *PushIntent) Instance() string {
|
||||
return pi.instance
|
||||
}
|
||||
|
||||
func (pi *PushIntent) Reduce(events ...*StorageEvent) error {
|
||||
if pi.reducer == nil {
|
||||
return nil
|
||||
}
|
||||
return pi.reducer.Reduce(events...)
|
||||
}
|
||||
|
||||
func (pi *PushIntent) Tx() *sql.Tx {
|
||||
return pi.tx
|
||||
}
|
||||
|
||||
func (pi *PushIntent) Aggregates() []*PushAggregate {
|
||||
return pi.aggregates
|
||||
}
|
||||
|
||||
type PushOpt func(pi *PushIntent)
|
||||
|
||||
func PushReducer(reducer Reducer) PushOpt {
|
||||
return func(pi *PushIntent) {
|
||||
pi.reducer = reducer
|
||||
}
|
||||
}
|
||||
|
||||
func PushTx(tx *sql.Tx) PushOpt {
|
||||
return func(pi *PushIntent) {
|
||||
pi.tx = tx
|
||||
}
|
||||
}
|
||||
|
||||
func AppendAggregate(owner, typ, id string, opts ...PushAggregateOpt) PushOpt {
|
||||
return AppendAggregates(NewPushAggregate(owner, typ, id, opts...))
|
||||
}
|
||||
|
||||
func AppendAggregates(aggregates ...*PushAggregate) PushOpt {
|
||||
return func(pi *PushIntent) {
|
||||
for _, aggregate := range aggregates {
|
||||
aggregate.parent = pi
|
||||
}
|
||||
pi.aggregates = append(pi.aggregates, aggregates...)
|
||||
}
|
||||
}
|
||||
|
||||
type PushAggregate struct {
|
||||
parent *PushIntent
|
||||
// typ of the aggregate
|
||||
typ string
|
||||
// id of the aggregate
|
||||
id string
|
||||
// owner of the aggregate
|
||||
owner string
|
||||
// Commands is an ordered list of changes on the aggregate
|
||||
commands []*Command
|
||||
// CurrentSequence checks the current state of the aggregate.
|
||||
// The following types match the current sequence of the aggregate as described:
|
||||
// * nil or [SequenceIgnore]: Not relevant to add the commands
|
||||
// * [SequenceMatches]: Must exactly match
|
||||
// * [SequenceAtLeast]: Must be >= the given sequence
|
||||
currentSequence CurrentSequence
|
||||
}
|
||||
|
||||
func NewPushAggregate(owner, typ, id string, opts ...PushAggregateOpt) *PushAggregate {
|
||||
pa := &PushAggregate{
|
||||
typ: typ,
|
||||
id: id,
|
||||
owner: owner,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(pa)
|
||||
}
|
||||
|
||||
return pa
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) Type() string {
|
||||
return pa.typ
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) ID() string {
|
||||
return pa.id
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) Owner() string {
|
||||
return pa.owner
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) Commands() []*Command {
|
||||
return pa.commands
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) Aggregate() *Aggregate {
|
||||
return &Aggregate{
|
||||
ID: pa.id,
|
||||
Type: pa.typ,
|
||||
Owner: pa.owner,
|
||||
Instance: pa.parent.instance,
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PushAggregate) CurrentSequence() CurrentSequence {
|
||||
return pa.currentSequence
|
||||
}
|
||||
|
||||
type PushAggregateOpt func(pa *PushAggregate)
|
||||
|
||||
func SetCurrentSequence(currentSequence CurrentSequence) PushAggregateOpt {
|
||||
return func(pa *PushAggregate) {
|
||||
pa.currentSequence = currentSequence
|
||||
}
|
||||
}
|
||||
|
||||
func IgnoreCurrentSequence() PushAggregateOpt {
|
||||
return func(pa *PushAggregate) {
|
||||
pa.currentSequence = SequenceIgnore()
|
||||
}
|
||||
}
|
||||
|
||||
func CurrentSequenceMatches(sequence uint32) PushAggregateOpt {
|
||||
return func(pa *PushAggregate) {
|
||||
pa.currentSequence = SequenceMatches(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
func CurrentSequenceAtLeast(sequence uint32) PushAggregateOpt {
|
||||
return func(pa *PushAggregate) {
|
||||
pa.currentSequence = SequenceAtLeast(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
func AppendCommands(commands ...*Command) PushAggregateOpt {
|
||||
return func(pa *PushAggregate) {
|
||||
pa.commands = append(pa.commands, commands...)
|
||||
}
|
||||
}
|
821
apps/api/internal/v2/eventstore/query.go
Normal file
821
apps/api/internal/v2/eventstore/query.go
Normal file
@@ -0,0 +1,821 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/database"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
healthier
|
||||
Query(ctx context.Context, query *Query) (eventCount int, err error)
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
instances *filter[[]string]
|
||||
filters []*Filter
|
||||
tx *sql.Tx
|
||||
pagination *Pagination
|
||||
reducer Reducer
|
||||
// TODO: await push
|
||||
}
|
||||
|
||||
func (q *Query) Instance() database.Condition {
|
||||
return q.instances.condition
|
||||
}
|
||||
|
||||
func (q *Query) Filters() []*Filter {
|
||||
return q.filters
|
||||
}
|
||||
|
||||
func (q *Query) Tx() *sql.Tx {
|
||||
return q.tx
|
||||
}
|
||||
|
||||
func (q *Query) Pagination() *Pagination {
|
||||
q.ensurePagination()
|
||||
return q.pagination
|
||||
}
|
||||
|
||||
func (q *Query) Reduce(events ...*StorageEvent) error {
|
||||
return q.reducer.Reduce(events...)
|
||||
}
|
||||
|
||||
func NewQuery(instance string, reducer Reducer, opts ...QueryOpt) *Query {
|
||||
query := &Query{
|
||||
reducer: reducer,
|
||||
}
|
||||
|
||||
for _, opt := range append([]QueryOpt{SetInstance(instance)}, opts...) {
|
||||
opt(query)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
type QueryOpt func(q *Query)
|
||||
|
||||
func SetInstance(instance string) QueryOpt {
|
||||
return InstancesEqual(instance)
|
||||
}
|
||||
|
||||
func InstancesEqual(instances ...string) QueryOpt {
|
||||
return func(q *Query) {
|
||||
var cond database.Condition
|
||||
switch len(instances) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(instances[0])
|
||||
default:
|
||||
cond = database.NewListEquals(instances...)
|
||||
}
|
||||
q.instances = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &instances,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InstancesContains(instances ...string) QueryOpt {
|
||||
return func(f *Query) {
|
||||
var cond database.Condition
|
||||
switch len(instances) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(instances[0])
|
||||
default:
|
||||
cond = database.NewListContains(instances...)
|
||||
}
|
||||
|
||||
f.instances = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &instances,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InstancesNotContains(instances ...string) QueryOpt {
|
||||
return func(f *Query) {
|
||||
var cond database.Condition
|
||||
switch len(instances) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextUnequal(instances[0])
|
||||
default:
|
||||
cond = database.NewListNotContains(instances...)
|
||||
}
|
||||
f.instances = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &instances,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetQueryTx(tx *sql.Tx) QueryOpt {
|
||||
return func(query *Query) {
|
||||
query.tx = tx
|
||||
}
|
||||
}
|
||||
|
||||
func QueryPagination(opts ...paginationOpt) QueryOpt {
|
||||
return func(query *Query) {
|
||||
query.ensurePagination()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(query.pagination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) ensurePagination() {
|
||||
if q.pagination != nil {
|
||||
return
|
||||
}
|
||||
q.pagination = new(Pagination)
|
||||
}
|
||||
|
||||
func AppendFilters(filters ...*Filter) QueryOpt {
|
||||
return func(query *Query) {
|
||||
for _, filter := range filters {
|
||||
filter.parent = query
|
||||
}
|
||||
query.filters = append(query.filters, filters...)
|
||||
}
|
||||
}
|
||||
|
||||
func SetFilters(filters ...*Filter) QueryOpt {
|
||||
return func(query *Query) {
|
||||
for _, filter := range filters {
|
||||
filter.parent = query
|
||||
}
|
||||
query.filters = filters
|
||||
}
|
||||
}
|
||||
|
||||
func AppendFilter(opts ...FilterOpt) QueryOpt {
|
||||
return AppendFilters(NewFilter(opts...))
|
||||
}
|
||||
|
||||
var ErrFilterMerge = errors.New("merge failed")
|
||||
|
||||
type FilterCreator func() []*Filter
|
||||
|
||||
func MergeFilters(filters ...[]*Filter) []*Filter {
|
||||
// TODO: improve merge by checking fields of filters and merge filters if possible
|
||||
// this will reduce cost of queries which do multiple filters
|
||||
return slices.Concat(filters...)
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
parent *Query
|
||||
pagination *Pagination
|
||||
|
||||
aggregateFilters []*AggregateFilter
|
||||
}
|
||||
|
||||
func (f *Filter) Parent() *Query {
|
||||
return f.parent
|
||||
}
|
||||
|
||||
func (f *Filter) Pagination() *Pagination {
|
||||
if f.pagination == nil {
|
||||
return f.parent.Pagination()
|
||||
}
|
||||
return f.pagination
|
||||
}
|
||||
|
||||
func (f *Filter) AggregateFilters() []*AggregateFilter {
|
||||
return f.aggregateFilters
|
||||
}
|
||||
|
||||
func NewFilter(opts ...FilterOpt) *Filter {
|
||||
f := new(Filter)
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
type FilterOpt func(f *Filter)
|
||||
|
||||
func AppendAggregateFilter(typ string, opts ...AggregateFilterOpt) FilterOpt {
|
||||
return AppendAggregateFilters(NewAggregateFilter(typ, opts...))
|
||||
}
|
||||
|
||||
func AppendAggregateFilters(filters ...*AggregateFilter) FilterOpt {
|
||||
return func(mf *Filter) {
|
||||
mf.aggregateFilters = append(mf.aggregateFilters, filters...)
|
||||
}
|
||||
}
|
||||
|
||||
func SetAggregateFilters(filters ...*AggregateFilter) FilterOpt {
|
||||
return func(mf *Filter) {
|
||||
mf.aggregateFilters = filters
|
||||
}
|
||||
}
|
||||
|
||||
func FilterPagination(opts ...paginationOpt) FilterOpt {
|
||||
return func(filter *Filter) {
|
||||
filter.ensurePagination()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(filter.pagination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) ensurePagination() {
|
||||
if f.pagination != nil {
|
||||
return
|
||||
}
|
||||
f.pagination = new(Pagination)
|
||||
}
|
||||
|
||||
func NewAggregateFilter(typ string, opts ...AggregateFilterOpt) *AggregateFilter {
|
||||
filter := &AggregateFilter{
|
||||
typ: typ,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(filter)
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
type AggregateFilter struct {
|
||||
typ string
|
||||
ids []string
|
||||
owners *filter[[]string]
|
||||
events []*EventFilter
|
||||
}
|
||||
|
||||
func (f *AggregateFilter) Type() *database.TextFilter[string] {
|
||||
return database.NewTextEqual(f.typ)
|
||||
}
|
||||
|
||||
func (f *AggregateFilter) IDs() database.Condition {
|
||||
if len(f.ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(f.ids) == 1 {
|
||||
return database.NewTextEqual(f.ids[0])
|
||||
}
|
||||
|
||||
return database.NewListContains(f.ids...)
|
||||
}
|
||||
|
||||
func (f *AggregateFilter) Owners() database.Condition {
|
||||
if f.owners == nil {
|
||||
return nil
|
||||
}
|
||||
return f.owners.condition
|
||||
}
|
||||
|
||||
func (f *AggregateFilter) Events() []*EventFilter {
|
||||
return f.events
|
||||
}
|
||||
|
||||
type AggregateFilterOpt func(f *AggregateFilter)
|
||||
|
||||
func SetAggregateID(id string) AggregateFilterOpt {
|
||||
return func(filter *AggregateFilter) {
|
||||
filter.ids = []string{id}
|
||||
}
|
||||
}
|
||||
|
||||
func AppendAggregateIDs(ids ...string) AggregateFilterOpt {
|
||||
return func(f *AggregateFilter) {
|
||||
f.ids = append(f.ids, ids...)
|
||||
}
|
||||
}
|
||||
|
||||
// AggregateIDs sets the given ids as search param
|
||||
func AggregateIDs(ids ...string) AggregateFilterOpt {
|
||||
return func(f *AggregateFilter) {
|
||||
f.ids = ids
|
||||
}
|
||||
}
|
||||
|
||||
func AggregateOwnersEqual(owners ...string) AggregateFilterOpt {
|
||||
return func(f *AggregateFilter) {
|
||||
var cond database.Condition
|
||||
switch len(owners) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(owners[0])
|
||||
default:
|
||||
cond = database.NewListEquals(owners...)
|
||||
}
|
||||
f.owners = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &owners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AggregateOwnersContains(owners ...string) AggregateFilterOpt {
|
||||
return func(f *AggregateFilter) {
|
||||
var cond database.Condition
|
||||
switch len(owners) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(owners[0])
|
||||
default:
|
||||
cond = database.NewListContains(owners...)
|
||||
}
|
||||
|
||||
f.owners = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &owners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AggregateOwnersNotContains(owners ...string) AggregateFilterOpt {
|
||||
return func(f *AggregateFilter) {
|
||||
var cond database.Condition
|
||||
switch len(owners) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextUnequal(owners[0])
|
||||
default:
|
||||
cond = database.NewListNotContains(owners...)
|
||||
}
|
||||
f.owners = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &owners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AppendEvent(opts ...EventFilterOpt) AggregateFilterOpt {
|
||||
return AppendEvents(NewEventFilter(opts...))
|
||||
}
|
||||
|
||||
func AppendEvents(events ...*EventFilter) AggregateFilterOpt {
|
||||
return func(filter *AggregateFilter) {
|
||||
filter.events = append(filter.events, events...)
|
||||
}
|
||||
}
|
||||
|
||||
func SetEvents(events ...*EventFilter) AggregateFilterOpt {
|
||||
return func(filter *AggregateFilter) {
|
||||
filter.events = events
|
||||
}
|
||||
}
|
||||
|
||||
func NewEventFilter(opts ...EventFilterOpt) *EventFilter {
|
||||
filter := new(EventFilter)
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(filter)
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
type EventFilter struct {
|
||||
types []string
|
||||
revision *filter[uint16]
|
||||
createdAt *filter[time.Time]
|
||||
sequence *filter[uint32]
|
||||
creators *filter[[]string]
|
||||
}
|
||||
|
||||
type filter[T any] struct {
|
||||
condition database.Condition
|
||||
// the following fields are considered as one of
|
||||
// you can either have value and max or value
|
||||
min, max *T
|
||||
value *T
|
||||
}
|
||||
|
||||
func (f *EventFilter) Types() database.Condition {
|
||||
switch len(f.types) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return database.NewTextEqual(f.types[0])
|
||||
default:
|
||||
return database.NewListContains(f.types...)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *EventFilter) Revision() database.Condition {
|
||||
if f.revision == nil {
|
||||
return nil
|
||||
}
|
||||
return f.revision.condition
|
||||
}
|
||||
|
||||
func (f *EventFilter) CreatedAt() database.Condition {
|
||||
if f.createdAt == nil {
|
||||
return nil
|
||||
}
|
||||
return f.createdAt.condition
|
||||
}
|
||||
|
||||
func (f *EventFilter) Sequence() database.Condition {
|
||||
if f.sequence == nil {
|
||||
return nil
|
||||
}
|
||||
return f.sequence.condition
|
||||
}
|
||||
|
||||
func (f *EventFilter) Creators() database.Condition {
|
||||
if f.creators == nil {
|
||||
return nil
|
||||
}
|
||||
return f.creators.condition
|
||||
}
|
||||
|
||||
type EventFilterOpt func(f *EventFilter)
|
||||
|
||||
func SetEventType(typ string) EventFilterOpt {
|
||||
return func(filter *EventFilter) {
|
||||
filter.types = []string{typ}
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventTypes overwrites the currently set types
|
||||
func SetEventTypes(types ...string) EventFilterOpt {
|
||||
return func(filter *EventFilter) {
|
||||
filter.types = types
|
||||
}
|
||||
}
|
||||
|
||||
// AppendEventTypes appends the types the currently set types
|
||||
func AppendEventTypes(types ...string) EventFilterOpt {
|
||||
return func(filter *EventFilter) {
|
||||
filter.types = append(filter.types, types...)
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionEquals(revision uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberEquals(revision),
|
||||
value: &revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionAtLeast(revision uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberAtLeast(revision),
|
||||
value: &revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionGreater(revision uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberGreater(revision),
|
||||
value: &revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionAtMost(revision uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberAtMost(revision),
|
||||
value: &revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionLess(revision uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberLess(revision),
|
||||
value: &revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventRevisionBetween(min, max uint16) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.revision = &filter[uint16]{
|
||||
condition: database.NewNumberBetween(min, max),
|
||||
min: &min,
|
||||
max: &max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtEquals(createdAt time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberEquals(createdAt),
|
||||
value: &createdAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtAtLeast(createdAt time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberAtLeast(createdAt),
|
||||
value: &createdAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtGreater(createdAt time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberGreater(createdAt),
|
||||
value: &createdAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtAtMost(createdAt time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberAtMost(createdAt),
|
||||
value: &createdAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtLess(createdAt time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberLess(createdAt),
|
||||
value: &createdAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatedAtBetween(min, max time.Time) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.createdAt = &filter[time.Time]{
|
||||
condition: database.NewNumberBetween(min, max),
|
||||
min: &min,
|
||||
max: &max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceEquals(sequence uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberEquals(sequence),
|
||||
value: &sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceAtLeast(sequence uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberAtLeast(sequence),
|
||||
value: &sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceGreater(sequence uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberGreater(sequence),
|
||||
value: &sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceAtMost(sequence uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberAtMost(sequence),
|
||||
value: &sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceLess(sequence uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberLess(sequence),
|
||||
value: &sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventSequenceBetween(min, max uint32) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
f.sequence = &filter[uint32]{
|
||||
condition: database.NewNumberBetween(min, max),
|
||||
min: &min,
|
||||
max: &max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatorsEqual(creators ...string) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
var cond database.Condition
|
||||
switch len(creators) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(creators[0])
|
||||
default:
|
||||
cond = database.NewListEquals(creators...)
|
||||
}
|
||||
f.creators = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &creators,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatorsContains(creators ...string) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
var cond database.Condition
|
||||
switch len(creators) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextEqual(creators[0])
|
||||
default:
|
||||
cond = database.NewListContains(creators...)
|
||||
}
|
||||
|
||||
f.creators = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &creators,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EventCreatorsNotContains(creators ...string) EventFilterOpt {
|
||||
return func(f *EventFilter) {
|
||||
var cond database.Condition
|
||||
switch len(creators) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
cond = database.NewTextUnequal(creators[0])
|
||||
default:
|
||||
cond = database.NewListNotContains(creators...)
|
||||
}
|
||||
f.creators = &filter[[]string]{
|
||||
condition: cond,
|
||||
value: &creators,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Limit(limit uint32) paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
p.ensurePagination()
|
||||
|
||||
p.pagination.Limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
func Offset(offset uint32) paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
p.ensurePagination()
|
||||
|
||||
p.pagination.Offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
type PositionCondition struct {
|
||||
min, max *GlobalPosition
|
||||
}
|
||||
|
||||
func (pc *PositionCondition) Max() *GlobalPosition {
|
||||
if pc == nil || pc.max == nil {
|
||||
return nil
|
||||
}
|
||||
max := *pc.max
|
||||
return &max
|
||||
}
|
||||
|
||||
func (pc *PositionCondition) Min() *GlobalPosition {
|
||||
if pc == nil || pc.min == nil {
|
||||
return nil
|
||||
}
|
||||
min := *pc.min
|
||||
return &min
|
||||
}
|
||||
|
||||
// PositionGreater prepares the condition as follows
|
||||
// if inPositionOrder is set: position = AND in_tx_order > OR or position >
|
||||
// if inPositionOrder is NOT set: position >
|
||||
func PositionGreater(position decimal.Decimal, inPositionOrder uint32) paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
p.ensurePosition()
|
||||
p.position.min = &GlobalPosition{
|
||||
Position: position,
|
||||
InPositionOrder: inPositionOrder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GlobalPositionGreater prepares the condition as follows
|
||||
// if inPositionOrder is set: position = AND in_tx_order > OR or position >
|
||||
// if inPositionOrder is NOT set: position >
|
||||
func GlobalPositionGreater(position *GlobalPosition) paginationOpt {
|
||||
return PositionGreater(position.Position, position.InPositionOrder)
|
||||
}
|
||||
|
||||
// PositionLess prepares the condition as follows
|
||||
// if inPositionOrder is set: position = AND in_tx_order > OR or position >
|
||||
// if inPositionOrder is NOT set: position >
|
||||
func PositionLess(position decimal.Decimal, inPositionOrder uint32) paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
p.ensurePosition()
|
||||
p.position.max = &GlobalPosition{
|
||||
Position: position,
|
||||
InPositionOrder: inPositionOrder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PositionBetween(min, max *GlobalPosition) paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
GlobalPositionGreater(min)(p)
|
||||
GlobalPositionLess(max)(p)
|
||||
}
|
||||
}
|
||||
|
||||
// GlobalPositionLess prepares the condition as follows
|
||||
// if inPositionOrder is set: position = AND in_tx_order > OR or position >
|
||||
// if inPositionOrder is NOT set: position >
|
||||
func GlobalPositionLess(position *GlobalPosition) paginationOpt {
|
||||
return PositionLess(position.Position, position.InPositionOrder)
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
pagination *database.Pagination
|
||||
position *PositionCondition
|
||||
|
||||
desc bool
|
||||
}
|
||||
|
||||
type paginationOpt func(*Pagination)
|
||||
|
||||
func (p *Pagination) Pagination() *database.Pagination {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.pagination
|
||||
}
|
||||
|
||||
func (p *Pagination) Position() *PositionCondition {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.position
|
||||
}
|
||||
|
||||
func (p *Pagination) Desc() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.desc
|
||||
}
|
||||
|
||||
func (p *Pagination) ensurePagination() {
|
||||
if p.pagination != nil {
|
||||
return
|
||||
}
|
||||
p.pagination = new(database.Pagination)
|
||||
}
|
||||
|
||||
func (p *Pagination) ensurePosition() {
|
||||
if p.position != nil {
|
||||
return
|
||||
}
|
||||
p.position = new(PositionCondition)
|
||||
}
|
||||
|
||||
func Descending() paginationOpt {
|
||||
return func(p *Pagination) {
|
||||
p.desc = true
|
||||
}
|
||||
}
|
1065
apps/api/internal/v2/eventstore/query_test.go
Normal file
1065
apps/api/internal/v2/eventstore/query_test.go
Normal file
File diff suppressed because it is too large
Load Diff
80
apps/api/internal/v2/eventstore/unique_constraint.go
Normal file
80
apps/api/internal/v2/eventstore/unique_constraint.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package eventstore
|
||||
|
||||
type UniqueConstraint struct {
|
||||
// UniqueType is the table name for the unique constraint
|
||||
UniqueType string
|
||||
// UniqueField is the unique key
|
||||
UniqueField string
|
||||
// Action defines if unique constraint should be added or removed
|
||||
Action UniqueConstraintAction
|
||||
// ErrorMessage defines the translation file key for the error message
|
||||
ErrorMessage string
|
||||
// IsGlobal defines if the unique constraint is globally unique or just within a single instance
|
||||
IsGlobal bool
|
||||
}
|
||||
|
||||
type UniqueConstraintAction int8
|
||||
|
||||
const (
|
||||
UniqueConstraintAdd UniqueConstraintAction = iota
|
||||
UniqueConstraintRemove
|
||||
UniqueConstraintInstanceRemove
|
||||
|
||||
uniqueConstraintActionCount
|
||||
)
|
||||
|
||||
func (f UniqueConstraintAction) Valid() bool {
|
||||
return f >= 0 && f < uniqueConstraintActionCount
|
||||
}
|
||||
|
||||
func NewAddEventUniqueConstraint(
|
||||
uniqueType,
|
||||
uniqueField,
|
||||
errMessage string) *UniqueConstraint {
|
||||
return &UniqueConstraint{
|
||||
UniqueType: uniqueType,
|
||||
UniqueField: uniqueField,
|
||||
ErrorMessage: errMessage,
|
||||
Action: UniqueConstraintAdd,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoveUniqueConstraint(
|
||||
uniqueType,
|
||||
uniqueField string) *UniqueConstraint {
|
||||
return &UniqueConstraint{
|
||||
UniqueType: uniqueType,
|
||||
UniqueField: uniqueField,
|
||||
Action: UniqueConstraintRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoveInstanceUniqueConstraints() *UniqueConstraint {
|
||||
return &UniqueConstraint{
|
||||
Action: UniqueConstraintInstanceRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAddGlobalUniqueConstraint(
|
||||
uniqueType,
|
||||
uniqueField,
|
||||
errMessage string) *UniqueConstraint {
|
||||
return &UniqueConstraint{
|
||||
UniqueType: uniqueType,
|
||||
UniqueField: uniqueField,
|
||||
ErrorMessage: errMessage,
|
||||
IsGlobal: true,
|
||||
Action: UniqueConstraintAdd,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoveGlobalUniqueConstraint(
|
||||
uniqueType,
|
||||
uniqueField string) *UniqueConstraint {
|
||||
return &UniqueConstraint{
|
||||
UniqueType: uniqueType,
|
||||
UniqueField: uniqueField,
|
||||
IsGlobal: true,
|
||||
Action: UniqueConstraintRemove,
|
||||
}
|
||||
}
|
8
apps/api/internal/v2/instance/aggregate.go
Normal file
8
apps/api/internal/v2/instance/aggregate.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package instance
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
|
||||
const (
|
||||
AggregateType = string(instance.AggregateType)
|
||||
eventTypePrefix = AggregateType + "."
|
||||
)
|
65
apps/api/internal/v2/instance/domain_policy.go
Normal file
65
apps/api/internal/v2/instance/domain_policy.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/policy"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const DomainPolicyAddedType = eventTypePrefix + policy.DomainPolicyAddedTypeSuffix
|
||||
|
||||
type DomainPolicyAddedPayload policy.DomainPolicyAddedPayload
|
||||
|
||||
type DomainPolicyAddedEvent eventstore.Event[DomainPolicyAddedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPolicyAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPolicyAddedEvent) ActionType() string {
|
||||
return DomainPolicyAddedType
|
||||
}
|
||||
|
||||
func DomainPolicyAddedEventFromStorage(event *eventstore.StorageEvent) (e *DomainPolicyAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "INSTA-z1a7D", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPolicyAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPolicyAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainPolicyChangedType = eventTypePrefix + policy.DomainPolicyChangedTypeSuffix
|
||||
|
||||
type DomainPolicyChangedPayload policy.DomainPolicyChangedPayload
|
||||
|
||||
type DomainPolicyChangedEvent eventstore.Event[DomainPolicyChangedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPolicyChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPolicyChangedEvent) ActionType() string {
|
||||
return DomainPolicyChangedType
|
||||
}
|
||||
|
||||
func DomainPolicyChangedEventFromStorage(event *eventstore.StorageEvent) (e *DomainPolicyChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "INSTA-BTLhd", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPolicyChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPolicyChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/instance/removed.go
Normal file
27
apps/api/internal/v2/instance/removed.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const RemovedType = eventTypePrefix + "removed"
|
||||
|
||||
type RemovedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*RemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *RemovedEvent) ActionType() string {
|
||||
return RemovedType
|
||||
}
|
||||
|
||||
func RemovedEventFromStorage(event *eventstore.StorageEvent) (e *RemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "INSTA-xppIg", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &RemovedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
62
apps/api/internal/v2/org/added.go
Normal file
62
apps/api/internal/v2/org/added.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const AddedType = eventTypePrefix + "added"
|
||||
|
||||
type addedPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AddedEvent eventstore.Event[addedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*AddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *AddedEvent) ActionType() string {
|
||||
return AddedType
|
||||
}
|
||||
|
||||
func AddedEventFromStorage(event *eventstore.StorageEvent) (e *AddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Nf3tr", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[addedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const uniqueOrgName = "org_name"
|
||||
|
||||
func NewAddedCommand(ctx context.Context, name string) (*eventstore.Command, error) {
|
||||
if name = strings.TrimSpace(name); name == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
|
||||
}
|
||||
return &eventstore.Command{
|
||||
Action: eventstore.Action[any]{
|
||||
Creator: authz.GetCtxData(ctx).UserID,
|
||||
Type: AddedType,
|
||||
Revision: 1,
|
||||
Payload: addedPayload{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
UniqueConstraints: []*eventstore.UniqueConstraint{
|
||||
eventstore.NewAddEventUniqueConstraint(uniqueOrgName, name, "Errors.Org.AlreadyExists"),
|
||||
},
|
||||
}, nil
|
||||
}
|
22
apps/api/internal/v2/org/aggregate.go
Normal file
22
apps/api/internal/v2/org/aggregate.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "org"
|
||||
eventTypePrefix = AggregateType + "."
|
||||
)
|
||||
|
||||
func NewAggregate(ctx context.Context, id string) *eventstore.Aggregate {
|
||||
return &eventstore.Aggregate{
|
||||
ID: id,
|
||||
Type: AggregateType,
|
||||
Instance: authz.GetInstance(ctx).InstanceID(),
|
||||
Owner: authz.GetCtxData(ctx).OrgID,
|
||||
}
|
||||
}
|
37
apps/api/internal/v2/org/changed.go
Normal file
37
apps/api/internal/v2/org/changed.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const ChangedType = eventTypePrefix + "changed"
|
||||
|
||||
type changedPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ChangedEvent eventstore.Event[changedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*ChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *ChangedEvent) ActionType() string {
|
||||
return ChangedType
|
||||
}
|
||||
|
||||
func ChangedEventFromStorage(event *eventstore.StorageEvent) (e *ChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-pzOfP", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[changedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/org/deactivated.go
Normal file
27
apps/api/internal/v2/org/deactivated.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const DeactivatedType = eventTypePrefix + "deactivated"
|
||||
|
||||
type DeactivatedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DeactivatedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DeactivatedEvent) ActionType() string {
|
||||
return DeactivatedType
|
||||
}
|
||||
|
||||
func DeactivatedEventFromStorage(event *eventstore.StorageEvent) (e *DeactivatedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-4zeWH", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &DeactivatedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
123
apps/api/internal/v2/org/domain.go
Normal file
123
apps/api/internal/v2/org/domain.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const DomainAddedType = "org." + domain.AddedTypeSuffix
|
||||
|
||||
type DomainAddedPayload domain.AddedPayload
|
||||
|
||||
type DomainAddedEvent eventstore.Event[DomainAddedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainAddedEvent) ActionType() string {
|
||||
return DomainAddedType
|
||||
}
|
||||
|
||||
func DomainAddedEventFromStorage(event *eventstore.StorageEvent) (e *DomainAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-CXVe3", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainVerifiedType = "org." + domain.VerifiedTypeSuffix
|
||||
|
||||
type DomainVerifiedPayload domain.VerifiedPayload
|
||||
|
||||
type DomainVerifiedEvent eventstore.Event[DomainVerifiedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainVerifiedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainVerifiedEvent) ActionType() string {
|
||||
return DomainVerifiedType
|
||||
}
|
||||
|
||||
func DomainVerifiedEventFromStorage(event *eventstore.StorageEvent) (e *DomainVerifiedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-RAwdb", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainVerifiedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainVerifiedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainPrimarySetType = "org." + domain.PrimarySetTypeSuffix
|
||||
|
||||
type DomainPrimarySetPayload domain.PrimarySetPayload
|
||||
|
||||
type DomainPrimarySetEvent eventstore.Event[DomainPrimarySetPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPrimarySetEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPrimarySetEvent) ActionType() string {
|
||||
return DomainPrimarySetType
|
||||
}
|
||||
|
||||
func DomainPrimarySetEventFromStorage(event *eventstore.StorageEvent) (e *DomainPrimarySetEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-7P3Iz", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPrimarySetPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPrimarySetEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainRemovedType = "org." + domain.RemovedTypeSuffix
|
||||
|
||||
type DomainRemovedPayload domain.RemovedPayload
|
||||
|
||||
type DomainRemovedEvent eventstore.Event[DomainRemovedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainRemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainRemovedEvent) ActionType() string {
|
||||
return DomainRemovedType
|
||||
}
|
||||
|
||||
func DomainRemovedEventFromStorage(event *eventstore.StorageEvent) (e *DomainRemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-ndpL2", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainRemovedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainRemovedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
94
apps/api/internal/v2/org/domain_policy.go
Normal file
94
apps/api/internal/v2/org/domain_policy.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/policy"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const DomainPolicyAddedType = eventTypePrefix + policy.DomainPolicyAddedTypeSuffix
|
||||
|
||||
type DomainPolicyAddedPayload policy.DomainPolicyAddedPayload
|
||||
|
||||
type DomainPolicyAddedEvent eventstore.Event[DomainPolicyAddedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPolicyAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPolicyAddedEvent) ActionType() string {
|
||||
return DomainPolicyAddedType
|
||||
}
|
||||
|
||||
func DomainPolicyAddedEventFromStorage(event *eventstore.StorageEvent) (e *DomainPolicyAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-asiSN", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPolicyAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPolicyAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainPolicyChangedType = eventTypePrefix + policy.DomainPolicyChangedTypeSuffix
|
||||
|
||||
type DomainPolicyChangedPayload policy.DomainPolicyChangedPayload
|
||||
|
||||
type DomainPolicyChangedEvent eventstore.Event[DomainPolicyChangedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPolicyChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPolicyChangedEvent) ActionType() string {
|
||||
return DomainPolicyChangedType
|
||||
}
|
||||
|
||||
func DomainPolicyChangedEventFromStorage(event *eventstore.StorageEvent) (e *DomainPolicyChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-BmN6K", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPolicyChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPolicyChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DomainPolicyRemovedType = eventTypePrefix + policy.DomainPolicyRemovedTypeSuffix
|
||||
|
||||
type DomainPolicyRemovedPayload policy.DomainPolicyRemovedPayload
|
||||
|
||||
type DomainPolicyRemovedEvent eventstore.Event[DomainPolicyRemovedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainPolicyRemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainPolicyRemovedEvent) ActionType() string {
|
||||
return DomainPolicyRemovedType
|
||||
}
|
||||
|
||||
func DomainPolicyRemovedEventFromStorage(event *eventstore.StorageEvent) (e *DomainPolicyRemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-nHy4z", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[DomainPolicyRemovedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainPolicyRemovedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/org/reactivated.go
Normal file
27
apps/api/internal/v2/org/reactivated.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const ReactivatedType = eventTypePrefix + "reactivated"
|
||||
|
||||
type ReactivatedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*ReactivatedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *ReactivatedEvent) ActionType() string {
|
||||
return ReactivatedType
|
||||
}
|
||||
|
||||
func ReactivatedEventFromStorage(event *eventstore.StorageEvent) (e *ReactivatedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-cPWZw", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &ReactivatedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/org/removed.go
Normal file
27
apps/api/internal/v2/org/removed.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const RemovedType = eventTypePrefix + "removed"
|
||||
|
||||
type RemovedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*RemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *RemovedEvent) ActionType() string {
|
||||
return RemovedType
|
||||
}
|
||||
|
||||
func RemovedEventFromStorage(event *eventstore.StorageEvent) (e *RemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-RSPYk", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &RemovedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
36
apps/api/internal/v2/org/state.go
Normal file
36
apps/api/internal/v2/org/state.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package org
|
||||
|
||||
type State uint8
|
||||
|
||||
const (
|
||||
UndefinedState State = iota
|
||||
ActiveState
|
||||
InactiveState
|
||||
RemovedState
|
||||
maxState
|
||||
)
|
||||
|
||||
func (s State) IsValid() bool {
|
||||
return s != UndefinedState ||
|
||||
s < maxState
|
||||
}
|
||||
|
||||
func (s State) Is(state State) bool {
|
||||
return s == state
|
||||
}
|
||||
|
||||
func (s State) IsValidState(state State) bool {
|
||||
return s.IsValid() && s.Is(state)
|
||||
}
|
||||
|
||||
func (s State) IsValidStates(states ...State) bool {
|
||||
if !s.IsValid() {
|
||||
return false
|
||||
}
|
||||
for _, state := range states {
|
||||
if s.Is(state) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
23
apps/api/internal/v2/policy/domain.go
Normal file
23
apps/api/internal/v2/policy/domain.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package policy
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
|
||||
const DomainPolicyAddedTypeSuffix = "policy.domain.added"
|
||||
|
||||
type DomainPolicyAddedPayload struct {
|
||||
UserLoginMustBeDomain bool `json:"userLoginMustBeDomain,omitempty"`
|
||||
ValidateOrgDomains bool `json:"validateOrgDomains,omitempty"`
|
||||
SMTPSenderAddressMatchesInstanceDomain bool `json:"smtpSenderAddressMatchesInstanceDomain,omitempty"`
|
||||
}
|
||||
|
||||
const DomainPolicyChangedTypeSuffix = "policy.domain.changed"
|
||||
|
||||
type DomainPolicyChangedPayload struct {
|
||||
UserLoginMustBeDomain *bool `json:"userLoginMustBeDomain,omitempty"`
|
||||
ValidateOrgDomains *bool `json:"validateOrgDomains,omitempty"`
|
||||
SMTPSenderAddressMatchesInstanceDomain *bool `json:"smtpSenderAddressMatchesInstanceDomain,omitempty"`
|
||||
}
|
||||
|
||||
const DomainPolicyRemovedTypeSuffix = "policy.domain.removed"
|
||||
|
||||
type DomainPolicyRemovedPayload eventstore.EmptyPayload
|
15
apps/api/internal/v2/projection/highest_position.go
Normal file
15
apps/api/internal/v2/projection/highest_position.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
type HighestPosition eventstore.GlobalPosition
|
||||
|
||||
var _ eventstore.Reducer = (*HighestPosition)(nil)
|
||||
|
||||
// Reduce implements eventstore.Reducer.
|
||||
func (h *HighestPosition) Reduce(events ...*eventstore.StorageEvent) error {
|
||||
*h = HighestPosition(events[len(events)-1].Position)
|
||||
return nil
|
||||
}
|
57
apps/api/internal/v2/projection/org_primary_domain.go
Normal file
57
apps/api/internal/v2/projection/org_primary_domain.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/org"
|
||||
)
|
||||
|
||||
type OrgPrimaryDomain struct {
|
||||
projection
|
||||
|
||||
id string
|
||||
|
||||
Domain string
|
||||
}
|
||||
|
||||
func NewOrgPrimaryDomain(id string) *OrgPrimaryDomain {
|
||||
return &OrgPrimaryDomain{
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrgPrimaryDomain) Filter() []*eventstore.Filter {
|
||||
return []*eventstore.Filter{
|
||||
eventstore.NewFilter(
|
||||
eventstore.FilterPagination(
|
||||
eventstore.GlobalPositionGreater(&p.position),
|
||||
),
|
||||
eventstore.AppendAggregateFilter(
|
||||
org.AggregateType,
|
||||
eventstore.AggregateIDs(p.id),
|
||||
eventstore.AppendEvent(
|
||||
eventstore.SetEventTypes(org.DomainPrimarySetType),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrgPrimaryDomain) Reduce(events ...*eventstore.StorageEvent) error {
|
||||
for _, event := range events {
|
||||
if !p.shouldReduce(event) {
|
||||
continue
|
||||
}
|
||||
if event.Type != org.DomainPrimarySetType {
|
||||
continue
|
||||
}
|
||||
e, err := org.DomainPrimarySetEventFromStorage(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Domain = e.Payload.Name
|
||||
p.projection.reduce(event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
67
apps/api/internal/v2/projection/org_state.go
Normal file
67
apps/api/internal/v2/projection/org_state.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/org"
|
||||
)
|
||||
|
||||
type OrgState struct {
|
||||
projection
|
||||
|
||||
id string
|
||||
|
||||
org.State
|
||||
}
|
||||
|
||||
func NewStateProjection(id string) *OrgState {
|
||||
// TODO: check buffer for id and return from buffer if exists
|
||||
return &OrgState{
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrgState) Filter() []*eventstore.Filter {
|
||||
return []*eventstore.Filter{
|
||||
eventstore.NewFilter(
|
||||
eventstore.FilterPagination(
|
||||
eventstore.Descending(),
|
||||
eventstore.GlobalPositionGreater(&p.position),
|
||||
),
|
||||
eventstore.AppendAggregateFilter(
|
||||
org.AggregateType,
|
||||
eventstore.AggregateIDs(p.id),
|
||||
eventstore.AppendEvent(
|
||||
eventstore.SetEventTypes(
|
||||
org.AddedType,
|
||||
org.DeactivatedType,
|
||||
org.ReactivatedType,
|
||||
org.RemovedType,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrgState) Reduce(events ...*eventstore.StorageEvent) error {
|
||||
for _, event := range events {
|
||||
if !p.shouldReduce(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case org.AddedType:
|
||||
p.State = org.ActiveState
|
||||
case org.DeactivatedType:
|
||||
p.State = org.InactiveState
|
||||
case org.ReactivatedType:
|
||||
p.State = org.ActiveState
|
||||
case org.RemovedType:
|
||||
p.State = org.RemovedState
|
||||
default:
|
||||
continue
|
||||
}
|
||||
p.position = event.Position
|
||||
}
|
||||
return nil
|
||||
}
|
20
apps/api/internal/v2/projection/projection.go
Normal file
20
apps/api/internal/v2/projection/projection.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package projection
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
|
||||
type projection struct {
|
||||
instance string
|
||||
position eventstore.GlobalPosition
|
||||
}
|
||||
|
||||
func (p *projection) reduce(event *eventstore.StorageEvent) {
|
||||
if p.instance == "" {
|
||||
p.instance = event.Aggregate.Instance
|
||||
}
|
||||
p.position = event.Position
|
||||
}
|
||||
|
||||
func (p *projection) shouldReduce(event *eventstore.StorageEvent) bool {
|
||||
shouldReduce := p.instance == "" || p.instance == event.Aggregate.Instance
|
||||
return shouldReduce && p.position.IsLess(event.Position)
|
||||
}
|
75
apps/api/internal/v2/readmodel/last_successful_mirror.go
Normal file
75
apps/api/internal/v2/readmodel/last_successful_mirror.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package readmodel
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/system"
|
||||
"github.com/zitadel/zitadel/internal/v2/system/mirror"
|
||||
)
|
||||
|
||||
type LastSuccessfulMirror struct {
|
||||
ID string
|
||||
Position decimal.Decimal
|
||||
source string
|
||||
}
|
||||
|
||||
func NewLastSuccessfulMirror(source string) *LastSuccessfulMirror {
|
||||
return &LastSuccessfulMirror{
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
var _ eventstore.Reducer = (*LastSuccessfulMirror)(nil)
|
||||
|
||||
func (p *LastSuccessfulMirror) Filter() *eventstore.Filter {
|
||||
return eventstore.NewFilter(
|
||||
eventstore.AppendAggregateFilter(
|
||||
system.AggregateType,
|
||||
eventstore.AggregateOwnersEqual(system.AggregateOwner),
|
||||
eventstore.AppendEvent(
|
||||
eventstore.SetEventTypes(
|
||||
mirror.SucceededType,
|
||||
),
|
||||
eventstore.EventCreatorsEqual(mirror.Creator),
|
||||
),
|
||||
),
|
||||
eventstore.FilterPagination(
|
||||
eventstore.Descending(),
|
||||
eventstore.Limit(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Reduce implements eventstore.Reducer.
|
||||
func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err error) {
|
||||
for _, event := range events {
|
||||
if event.Type == mirror.SucceededType {
|
||||
err = h.reduceSucceeded(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error {
|
||||
// if position is set we skip all older events
|
||||
if h.Position.GreaterThan(decimal.NewFromInt(0)) {
|
||||
return nil
|
||||
|
||||
}
|
||||
succeededEvent, err := mirror.SucceededEventFromStorage(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.source != succeededEvent.Payload.Source {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.Position = succeededEvent.Payload.Position
|
||||
|
||||
return nil
|
||||
}
|
70
apps/api/internal/v2/readmodel/org.go
Normal file
70
apps/api/internal/v2/readmodel/org.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package readmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/org"
|
||||
"github.com/zitadel/zitadel/internal/v2/projection"
|
||||
)
|
||||
|
||||
type Org struct {
|
||||
ID string
|
||||
Name string
|
||||
PrimaryDomain *projection.OrgPrimaryDomain
|
||||
State *projection.OrgState
|
||||
|
||||
Sequence uint32
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Owner string
|
||||
InstanceID string
|
||||
}
|
||||
|
||||
func NewOrg(id string) *Org {
|
||||
return &Org{
|
||||
ID: id,
|
||||
State: projection.NewStateProjection(id),
|
||||
PrimaryDomain: projection.NewOrgPrimaryDomain(id),
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *Org) Filter() []*eventstore.Filter {
|
||||
return []*eventstore.Filter{
|
||||
// we don't need the filters of the projections as we filter all events of the read model
|
||||
eventstore.NewFilter(
|
||||
eventstore.AppendAggregateFilter(
|
||||
org.AggregateType,
|
||||
eventstore.SetAggregateID(rm.ID),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error {
|
||||
for _, event := range events {
|
||||
switch event.Type {
|
||||
case org.AddedType:
|
||||
added, err := org.AddedEventFromStorage(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rm.Name = added.Payload.Name
|
||||
rm.Owner = event.Aggregate.Owner
|
||||
rm.CreationDate = event.CreatedAt
|
||||
case org.ChangedType:
|
||||
changed, err := org.ChangedEventFromStorage(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rm.Name = changed.Payload.Name
|
||||
}
|
||||
rm.Sequence = event.Sequence
|
||||
rm.ChangeDate = event.CreatedAt
|
||||
rm.InstanceID = event.Aggregate.Instance
|
||||
}
|
||||
if err := rm.State.Reduce(events...); err != nil {
|
||||
return err
|
||||
}
|
||||
return rm.PrimaryDomain.Reduce(events...)
|
||||
}
|
15
apps/api/internal/v2/readmodel/query.go
Normal file
15
apps/api/internal/v2/readmodel/query.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package readmodel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
type QueryOpt func(opts []eventstore.QueryOpt) []eventstore.QueryOpt
|
||||
|
||||
func WithTx(tx *sql.Tx) QueryOpt {
|
||||
return func(opts []eventstore.QueryOpt) []eventstore.QueryOpt {
|
||||
return append(opts, eventstore.SetQueryTx(tx))
|
||||
}
|
||||
}
|
8
apps/api/internal/v2/system/aggregate.go
Normal file
8
apps/api/internal/v2/system/aggregate.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package system
|
||||
|
||||
const (
|
||||
AggregateType = "system"
|
||||
AggregateOwner = "SYSTEM"
|
||||
AggregateInstance = ""
|
||||
EventTypePrefix = AggregateType + "."
|
||||
)
|
44
apps/api/internal/v2/system/event.go
Normal file
44
apps/api/internal/v2/system/event.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, IDGeneratedType, eventstore.GenericEventMapper[IDGeneratedEvent])
|
||||
}
|
||||
|
||||
const IDGeneratedType = AggregateType + ".id.generated"
|
||||
|
||||
type IDGeneratedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIDGeneratedEvent(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) *IDGeneratedEvent {
|
||||
return &IDGeneratedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
eventstore.NewAggregate(ctx, AggregateOwner, AggregateType, "v1"),
|
||||
IDGeneratedType),
|
||||
ID: id,
|
||||
}
|
||||
}
|
8
apps/api/internal/v2/system/mirror/aggregate.go
Normal file
8
apps/api/internal/v2/system/mirror/aggregate.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package mirror
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/v2/system"
|
||||
|
||||
const (
|
||||
Creator = "MIRROR"
|
||||
eventTypePrefix = system.EventTypePrefix + "mirror."
|
||||
)
|
52
apps/api/internal/v2/system/mirror/failed.go
Normal file
52
apps/api/internal/v2/system/mirror/failed.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type failedPayload struct {
|
||||
Cause string `json:"cause"`
|
||||
// Source is the name of the database data are mirrored to
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
const FailedType = eventTypePrefix + "failed"
|
||||
|
||||
type FailedEvent eventstore.Event[failedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*FailedEvent)(nil)
|
||||
|
||||
func (e *FailedEvent) ActionType() string {
|
||||
return FailedType
|
||||
}
|
||||
|
||||
func FailedEventFromStorage(event *eventstore.StorageEvent) (e *FailedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-bwB9l", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[failedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FailedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewFailedCommand(source string, cause error) *eventstore.Command {
|
||||
return &eventstore.Command{
|
||||
Action: eventstore.Action[any]{
|
||||
Creator: Creator,
|
||||
Type: FailedType,
|
||||
Payload: failedPayload{
|
||||
Cause: cause.Error(),
|
||||
Source: source,
|
||||
},
|
||||
Revision: 1,
|
||||
},
|
||||
}
|
||||
}
|
68
apps/api/internal/v2/system/mirror/started.go
Normal file
68
apps/api/internal/v2/system/mirror/started.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type startedPayload struct {
|
||||
// Destination is the name of the database data are mirrored to
|
||||
Destination string `json:"destination"`
|
||||
// Either Instances or System needs to be set
|
||||
Instances []string `json:"instances,omitempty"`
|
||||
System bool `json:"system,omitempty"`
|
||||
}
|
||||
|
||||
const StartedType = eventTypePrefix + "started"
|
||||
|
||||
type StartedEvent eventstore.Event[startedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*StartedEvent)(nil)
|
||||
|
||||
func (e *StartedEvent) ActionType() string {
|
||||
return StartedType
|
||||
}
|
||||
|
||||
func StartedEventFromStorage(event *eventstore.StorageEvent) (e *StartedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-bwB9l", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[startedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StartedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewStartedSystemCommand(destination string) *eventstore.Command {
|
||||
return newStartedCommand(&startedPayload{
|
||||
Destination: destination,
|
||||
System: true,
|
||||
})
|
||||
}
|
||||
|
||||
func NewStartedInstancesCommand(destination string, instances []string) (*eventstore.Command, error) {
|
||||
if len(instances) == 0 {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-8YkrE", "Errors.Mirror.NoInstances")
|
||||
}
|
||||
return newStartedCommand(&startedPayload{
|
||||
Destination: destination,
|
||||
Instances: instances,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func newStartedCommand(payload *startedPayload) *eventstore.Command {
|
||||
return &eventstore.Command{
|
||||
Action: eventstore.Action[any]{
|
||||
Creator: Creator,
|
||||
Type: StartedType,
|
||||
Revision: 1,
|
||||
Payload: *payload,
|
||||
},
|
||||
}
|
||||
}
|
55
apps/api/internal/v2/system/mirror/succeeded.go
Normal file
55
apps/api/internal/v2/system/mirror/succeeded.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type succeededPayload struct {
|
||||
// Source is the name of the database data are mirrored from
|
||||
Source string `json:"source"`
|
||||
// Position until data will be mirrored
|
||||
Position decimal.Decimal `json:"position"`
|
||||
}
|
||||
|
||||
const SucceededType = eventTypePrefix + "succeeded"
|
||||
|
||||
type SucceededEvent eventstore.Event[succeededPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*SucceededEvent)(nil)
|
||||
|
||||
func (e *SucceededEvent) ActionType() string {
|
||||
return SucceededType
|
||||
}
|
||||
|
||||
func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-xh5IW", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[succeededPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SucceededEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSucceededCommand(source string, position decimal.Decimal) *eventstore.Command {
|
||||
return &eventstore.Command{
|
||||
Action: eventstore.Action[any]{
|
||||
Creator: Creator,
|
||||
Type: SucceededType,
|
||||
Revision: 1,
|
||||
Payload: succeededPayload{
|
||||
Source: source,
|
||||
Position: position,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
23
apps/api/internal/v2/user/aggregate.go
Normal file
23
apps/api/internal/v2/user/aggregate.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "user"
|
||||
humanPrefix = AggregateType + ".human"
|
||||
machinePrefix = AggregateType + ".machine"
|
||||
)
|
||||
|
||||
func NewAggregate(ctx context.Context, id string) *eventstore.Aggregate {
|
||||
return &eventstore.Aggregate{
|
||||
ID: id,
|
||||
Type: AggregateType,
|
||||
Instance: authz.GetInstance(ctx).InstanceID(),
|
||||
Owner: authz.GetCtxData(ctx).OrgID,
|
||||
}
|
||||
}
|
38
apps/api/internal/v2/user/domain_claimed.go
Normal file
38
apps/api/internal/v2/user/domain_claimed.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type domainClaimedPayload struct {
|
||||
Username string `json:"userName"`
|
||||
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
|
||||
}
|
||||
|
||||
type DomainClaimedEvent eventstore.Event[domainClaimedPayload]
|
||||
|
||||
const DomainClaimedType = AggregateType + ".domain.claimed.sent"
|
||||
|
||||
var _ eventstore.TypeChecker = (*DomainClaimedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DomainClaimedEvent) ActionType() string {
|
||||
return DomainClaimedType
|
||||
}
|
||||
|
||||
func DomainClaimedEventFromStorage(event *eventstore.StorageEvent) (e *DomainClaimedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-x8O4o", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[domainClaimedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DomainClaimedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
57
apps/api/internal/v2/user/human_added.go
Normal file
57
apps/api/internal/v2/user/human_added.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const HumanAddedType = AggregateType + ".human.added"
|
||||
|
||||
type humanAddedPayload struct {
|
||||
Username string `json:"userName"`
|
||||
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Gender domain.Gender `json:"gender,omitempty"`
|
||||
|
||||
EmailAddress domain.EmailAddress `json:"email,omitempty"`
|
||||
PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
|
||||
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
PasswordChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
type HumanAddedEvent eventstore.Event[humanAddedPayload]
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanAddedEvent) ActionType() string {
|
||||
return HumanAddedType
|
||||
}
|
||||
|
||||
func HumanAddedEventFromStorage(event *eventstore.StorageEvent) (e *HumanAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-MRZ3p", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
61
apps/api/internal/v2/user/human_avatar.go
Normal file
61
apps/api/internal/v2/user/human_avatar.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/avatar"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type HumanAvatarAddedEvent eventstore.Event[avatar.AddedPayload]
|
||||
|
||||
const HumanAvatarAddedType = humanPrefix + avatar.AvatarAddedTypeSuffix
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanAvatarAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanAvatarAddedEvent) ActionType() string {
|
||||
return HumanAvatarAddedType
|
||||
}
|
||||
|
||||
func HumanAvatarAddedEventFromStorage(event *eventstore.StorageEvent) (e *HumanAvatarAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-ddQaI", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[avatar.AddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanAvatarAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HumanAvatarRemovedEvent eventstore.Event[avatar.RemovedPayload]
|
||||
|
||||
const HumanAvatarRemovedType = humanPrefix + avatar.AvatarRemovedTypeSuffix
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanAvatarRemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanAvatarRemovedEvent) ActionType() string {
|
||||
return HumanAvatarRemovedType
|
||||
}
|
||||
|
||||
func HumanAvatarRemovedEventFromStorage(event *eventstore.StorageEvent) (e *HumanAvatarRemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-j2CkY", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[avatar.RemovedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanAvatarRemovedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
38
apps/api/internal/v2/user/human_email_changed.go
Normal file
38
apps/api/internal/v2/user/human_email_changed.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanEmailChangedPayload struct {
|
||||
Address domain.EmailAddress `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
type HumanEmailChangedEvent eventstore.Event[humanEmailChangedPayload]
|
||||
|
||||
const HumanEmailChangedType = humanPrefix + ".email.changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanEmailChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanEmailChangedEvent) ActionType() string {
|
||||
return HumanEmailChangedType
|
||||
}
|
||||
|
||||
func HumanEmailChangedEventFromStorage(event *eventstore.StorageEvent) (e *HumanEmailChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-Wr2lR", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanEmailChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanEmailChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/human_email_verified.go
Normal file
27
apps/api/internal/v2/user/human_email_verified.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type HumanEmailVerifiedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const HumanEmailVerifiedType = humanPrefix + ".email.verified"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanEmailVerifiedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanEmailVerifiedEvent) ActionType() string {
|
||||
return HumanEmailVerifiedType
|
||||
}
|
||||
|
||||
func HumanEmailVerifiedEventFromStorage(event *eventstore.StorageEvent) (e *HumanEmailVerifiedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-X3esB", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &HumanEmailVerifiedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
43
apps/api/internal/v2/user/human_init_code_added.go
Normal file
43
apps/api/internal/v2/user/human_init_code_added.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanInitCodeAddedPayload struct {
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
|
||||
AuthRequestID string `json:"authRequestID,omitempty"`
|
||||
}
|
||||
|
||||
type HumanInitCodeAddedEvent eventstore.Event[humanInitCodeAddedPayload]
|
||||
|
||||
const HumanInitCodeAddedType = humanPrefix + ".initialization.code.added"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanInitCodeAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanInitCodeAddedEvent) ActionType() string {
|
||||
return HumanInitCodeAddedType
|
||||
}
|
||||
|
||||
func HumanInitCodeAddedEventFromStorage(event *eventstore.StorageEvent) (e *HumanInitCodeAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-XaGf6", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanInitCodeAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanInitCodeAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/human_init_code_succeeded.go
Normal file
27
apps/api/internal/v2/user/human_init_code_succeeded.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type HumanInitCodeSucceededEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const HumanInitCodeSucceededType = humanPrefix + ".initialization.check.succeeded"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanInitCodeSucceededEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanInitCodeSucceededEvent) ActionType() string {
|
||||
return HumanInitCodeSucceededType
|
||||
}
|
||||
|
||||
func HumanInitCodeSucceededEventFromStorage(event *eventstore.StorageEvent) (e *HumanInitCodeSucceededEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-12A5m", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &HumanInitCodeSucceededEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
44
apps/api/internal/v2/user/human_password_changed.go
Normal file
44
apps/api/internal/v2/user/human_password_changed.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanPasswordChangedPayload struct {
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired"`
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
|
||||
}
|
||||
|
||||
type HumanPasswordChangedEvent eventstore.Event[humanPasswordChangedPayload]
|
||||
|
||||
const HumanPasswordChangedType = humanPrefix + ".password.changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanPasswordChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanPasswordChangedEvent) ActionType() string {
|
||||
return HumanPasswordChangedType
|
||||
}
|
||||
|
||||
func HumanPasswordChangedEventFromStorage(event *eventstore.StorageEvent) (e *HumanPasswordChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-Fx5tr", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanPasswordChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanPasswordChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
38
apps/api/internal/v2/user/human_phone_changed.go
Normal file
38
apps/api/internal/v2/user/human_phone_changed.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanPhoneChangedPayload struct {
|
||||
PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type HumanPhoneChangedEvent eventstore.Event[humanPhoneChangedPayload]
|
||||
|
||||
const HumanPhoneChangedType = humanPrefix + ".phone.changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanPhoneChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanPhoneChangedEvent) ActionType() string {
|
||||
return HumanPhoneChangedType
|
||||
}
|
||||
|
||||
func HumanPhoneChangedEventFromStorage(event *eventstore.StorageEvent) (e *HumanPhoneChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-d6hGS", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanPhoneChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanPhoneChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/human_phone_removed.go
Normal file
27
apps/api/internal/v2/user/human_phone_removed.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type HumanPhoneRemovedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const HumanPhoneRemovedType = humanPrefix + ".phone.removed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanPhoneRemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanPhoneRemovedEvent) ActionType() string {
|
||||
return HumanPhoneRemovedType
|
||||
}
|
||||
|
||||
func HumanPhoneRemovedEventFromStorage(event *eventstore.StorageEvent) (e *HumanPhoneRemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-vaD75", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &HumanPhoneRemovedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/human_phone_verified.go
Normal file
27
apps/api/internal/v2/user/human_phone_verified.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type HumanPhoneVerifiedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const HumanPhoneVerifiedType = humanPrefix + ".phone.removed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanPhoneVerifiedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanPhoneVerifiedEvent) ActionType() string {
|
||||
return HumanPhoneVerifiedType
|
||||
}
|
||||
|
||||
func HumanPhoneVerifiedEventFromStorage(event *eventstore.StorageEvent) (e *HumanPhoneVerifiedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-ycRBi", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &HumanPhoneVerifiedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
45
apps/api/internal/v2/user/human_profile_changed.go
Normal file
45
apps/api/internal/v2/user/human_profile_changed.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanProfileChangedPayload struct {
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
NickName *string `json:"nickName,omitempty"`
|
||||
DisplayName *string `json:"displayName,omitempty"`
|
||||
PreferredLanguage *language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Gender *domain.Gender `json:"gender,omitempty"`
|
||||
}
|
||||
|
||||
type HumanProfileChangedEvent eventstore.Event[humanProfileChangedPayload]
|
||||
|
||||
const HumanProfileChangedType = humanPrefix + ".profile.changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanProfileChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanProfileChangedEvent) ActionType() string {
|
||||
return HumanProfileChangedType
|
||||
}
|
||||
|
||||
func HumanProfileChangedEventFromStorage(event *eventstore.StorageEvent) (e *HumanProfileChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-Z1aFH", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanProfileChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanProfileChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
55
apps/api/internal/v2/user/human_registered.go
Normal file
55
apps/api/internal/v2/user/human_registered.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type humanRegisteredPayload struct {
|
||||
Username string `json:"userName"`
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Gender domain.Gender `json:"gender,omitempty"`
|
||||
EmailAddress domain.EmailAddress `json:"email,omitempty"`
|
||||
PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
|
||||
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"` // legacy
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
PasswordChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
type HumanRegisteredEvent eventstore.Event[humanRegisteredPayload]
|
||||
|
||||
const HumanRegisteredType = humanPrefix + ".selfregistered"
|
||||
|
||||
var _ eventstore.TypeChecker = (*HumanRegisteredEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *HumanRegisteredEvent) ActionType() string {
|
||||
return HumanRegisteredType
|
||||
}
|
||||
|
||||
func HumanRegisteredEventFromStorage(event *eventstore.StorageEvent) (e *HumanRegisteredEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-8HvGi", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[humanRegisteredPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HumanRegisteredEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
41
apps/api/internal/v2/user/machine_added.go
Normal file
41
apps/api/internal/v2/user/machine_added.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type machineAddedPayload struct {
|
||||
Username string `json:"userName"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AccessTokenType domain.OIDCTokenType `json:"accessTokenType,omitempty"`
|
||||
}
|
||||
|
||||
type MachineAddedEvent eventstore.Event[machineAddedPayload]
|
||||
|
||||
const MachineAddedType = machinePrefix + ".added"
|
||||
|
||||
var _ eventstore.TypeChecker = (*MachineAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *MachineAddedEvent) ActionType() string {
|
||||
return MachineAddedType
|
||||
}
|
||||
|
||||
func MachineAddedEventFromStorage(event *eventstore.StorageEvent) (e *MachineAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-WLLoW", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[machineAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MachineAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
40
apps/api/internal/v2/user/machine_changed.go
Normal file
40
apps/api/internal/v2/user/machine_changed.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type machineChangedPayload struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AccessTokenType *domain.OIDCTokenType `json:"accessTokenType,omitempty"`
|
||||
}
|
||||
|
||||
type MachineChangedEvent eventstore.Event[machineChangedPayload]
|
||||
|
||||
const MachineChangedType = machinePrefix + ".changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*MachineChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *MachineChangedEvent) ActionType() string {
|
||||
return MachineChangedType
|
||||
}
|
||||
|
||||
func MachineChangedEventFromStorage(event *eventstore.StorageEvent) (e *MachineChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-JHwNs", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[machineChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MachineChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
37
apps/api/internal/v2/user/machine_secret_hash_updated.go
Normal file
37
apps/api/internal/v2/user/machine_secret_hash_updated.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type machineSecretHashUpdatedPayload struct {
|
||||
HashedSecret string `json:"hashedSecret,omitempty"`
|
||||
}
|
||||
|
||||
type MachineSecretHashUpdatedEvent eventstore.Event[machineSecretHashUpdatedPayload]
|
||||
|
||||
const MachineSecretHashUpdatedType = machinePrefix + ".secret.updated"
|
||||
|
||||
var _ eventstore.TypeChecker = (*MachineSecretHashUpdatedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *MachineSecretHashUpdatedEvent) ActionType() string {
|
||||
return MachineSecretHashUpdatedType
|
||||
}
|
||||
|
||||
func MachineSecretHashUpdatedEventFromStorage(event *eventstore.StorageEvent) (e *MachineSecretHashUpdatedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-y41RK", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[machineSecretHashUpdatedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MachineSecretHashUpdatedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/machine_secret_removed.go
Normal file
27
apps/api/internal/v2/user/machine_secret_removed.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type MachineSecretRemovedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const MachineSecretRemovedType = machinePrefix + ".secret.removed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*MachineSecretRemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *MachineSecretRemovedEvent) ActionType() string {
|
||||
return MachineSecretRemovedType
|
||||
}
|
||||
|
||||
func MachineSecretRemovedEventFromStorage(event *eventstore.StorageEvent) (e *MachineSecretRemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-SMtct", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &MachineSecretRemovedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
41
apps/api/internal/v2/user/machine_secret_set.go
Normal file
41
apps/api/internal/v2/user/machine_secret_set.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type machineSecretSetPayload struct {
|
||||
// New events only use EncodedHash. However, the ClientSecret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"`
|
||||
HashedSecret string `json:"hashedSecret,omitempty"`
|
||||
}
|
||||
|
||||
type MachineSecretHashSetEvent eventstore.Event[machineSecretSetPayload]
|
||||
|
||||
const MachineSecretHashSetType = machinePrefix + ".secret.set"
|
||||
|
||||
var _ eventstore.TypeChecker = (*MachineSecretHashSetEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *MachineSecretHashSetEvent) ActionType() string {
|
||||
return MachineSecretHashSetType
|
||||
}
|
||||
|
||||
func MachineSecretHashSetEventFromStorage(event *eventstore.StorageEvent) (e *MachineSecretHashSetEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-DzycT", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[machineSecretSetPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MachineSecretHashSetEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
51
apps/api/internal/v2/user/token_added.go
Normal file
51
apps/api/internal/v2/user/token_added.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type tokenAddedPayload struct {
|
||||
TokenID string `json:"tokenId,omitempty"`
|
||||
ApplicationID string `json:"applicationId,omitempty"`
|
||||
UserAgentID string `json:"userAgentId,omitempty"`
|
||||
RefreshTokenID string `json:"refreshTokenID,omitempty"`
|
||||
Audience []string `json:"audience,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
AuthMethodsReferences []string `json:"authMethodsReferences,omitempty"`
|
||||
AuthTime time.Time `json:"authTime,omitempty"`
|
||||
Expiration time.Time `json:"expiration,omitempty"`
|
||||
PreferredLanguage string `json:"preferredLanguage,omitempty"`
|
||||
Reason domain.TokenReason `json:"reason,omitempty"`
|
||||
Actor *domain.TokenActor `json:"actor,omitempty"`
|
||||
}
|
||||
|
||||
type TokenAddedEvent eventstore.Event[tokenAddedPayload]
|
||||
|
||||
const TokenAddedType = AggregateType + ".token.added"
|
||||
|
||||
var _ eventstore.TypeChecker = (*TokenAddedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *TokenAddedEvent) ActionType() string {
|
||||
return TokenAddedType
|
||||
}
|
||||
|
||||
func TokenAddedEventFromStorage(event *eventstore.StorageEvent) (e *TokenAddedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-0YSt4", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[tokenAddedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TokenAddedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/user_deactivated.go
Normal file
27
apps/api/internal/v2/user/user_deactivated.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type DeactivatedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const DeactivatedType = AggregateType + ".deactivated"
|
||||
|
||||
var _ eventstore.TypeChecker = (*DeactivatedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *DeactivatedEvent) ActionType() string {
|
||||
return DeactivatedType
|
||||
}
|
||||
|
||||
func DeactivatedEventFromStorage(event *eventstore.StorageEvent) (e *DeactivatedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-SBLu2", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &DeactivatedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/user_locked.go
Normal file
27
apps/api/internal/v2/user/user_locked.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type LockedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const LockedType = AggregateType + ".locked"
|
||||
|
||||
var _ eventstore.TypeChecker = (*LockedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *LockedEvent) ActionType() string {
|
||||
return LockedType
|
||||
}
|
||||
|
||||
func LockedEventFromStorage(event *eventstore.StorageEvent) (e *LockedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-48jjE", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &LockedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/user_reactivated.go
Normal file
27
apps/api/internal/v2/user/user_reactivated.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type ReactivatedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const ReactivatedType = AggregateType + ".reactivated"
|
||||
|
||||
var _ eventstore.TypeChecker = (*ReactivatedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *ReactivatedEvent) ActionType() string {
|
||||
return ReactivatedType
|
||||
}
|
||||
|
||||
func ReactivatedEventFromStorage(event *eventstore.StorageEvent) (e *ReactivatedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-B3fcY", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &ReactivatedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/user_removed.go
Normal file
27
apps/api/internal/v2/user/user_removed.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type RemovedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const RemovedType = AggregateType + ".removed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*RemovedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *RemovedEvent) ActionType() string {
|
||||
return RemovedType
|
||||
}
|
||||
|
||||
func RemovedEventFromStorage(event *eventstore.StorageEvent) (e *RemovedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-UN6Xa", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &RemovedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
27
apps/api/internal/v2/user/user_unlocked.go
Normal file
27
apps/api/internal/v2/user/user_unlocked.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type UnlockedEvent eventstore.Event[eventstore.EmptyPayload]
|
||||
|
||||
const UnlockedType = AggregateType + ".unlocked"
|
||||
|
||||
var _ eventstore.TypeChecker = (*UnlockedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *UnlockedEvent) ActionType() string {
|
||||
return UnlockedType
|
||||
}
|
||||
|
||||
func UnlockedEventFromStorage(event *eventstore.StorageEvent) (e *UnlockedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-HB0wi", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
return &UnlockedEvent{
|
||||
StorageEvent: event,
|
||||
}, nil
|
||||
}
|
37
apps/api/internal/v2/user/username_changed.go
Normal file
37
apps/api/internal/v2/user/username_changed.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type usernameChangedPayload struct {
|
||||
Username string `json:"userName"`
|
||||
}
|
||||
|
||||
type UsernameChangedEvent eventstore.Event[usernameChangedPayload]
|
||||
|
||||
const UsernameChangedType = AggregateType + ".username.changed"
|
||||
|
||||
var _ eventstore.TypeChecker = (*UsernameChangedEvent)(nil)
|
||||
|
||||
// ActionType implements eventstore.Typer.
|
||||
func (c *UsernameChangedEvent) ActionType() string {
|
||||
return UsernameChangedType
|
||||
}
|
||||
|
||||
func UsernameChangedEventFromStorage(event *eventstore.StorageEvent) (e *UsernameChangedEvent, _ error) {
|
||||
if event.Type != e.ActionType() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "USER-hCGsh", "Errors.Invalid.Event.Type")
|
||||
}
|
||||
|
||||
payload, err := eventstore.UnmarshalPayload[usernameChangedPayload](event.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UsernameChangedEvent{
|
||||
StorageEvent: event,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
Reference in New Issue
Block a user