chore: move the go code into a subfolder

This commit is contained in:
Florian Forster
2025-08-05 15:20:32 -07:00
parent 4ad22ba456
commit cd2921de26
2978 changed files with 373 additions and 300 deletions

View File

@@ -0,0 +1,7 @@
package avatar
const AvatarAddedTypeSuffix = ".avatar.added"
type AddedPayload struct {
StoreKey string `json:"storeKey"`
}

View File

@@ -0,0 +1,7 @@
package avatar
const AvatarRemovedTypeSuffix = ".avatar.removed"
type RemovedPayload struct {
StoreKey string `json:"storeKey"`
}

View 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
}

View 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
)

View 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)
})
}
}

View 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()
}

View 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()
}

View 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
}

View 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])
}
}

View 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)
}
}

View 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)
})
}
}

View 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)
}

View 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
}

View 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)
}

View 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])
}
}
}

View 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
}

View 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)
})
}
}

View File

@@ -0,0 +1,7 @@
package domain
const AddedTypeSuffix = "domain.added"
type AddedPayload struct {
Name string `json:"domain"`
}

View File

@@ -0,0 +1,7 @@
package domain
const PrimarySetTypeSuffix = "domain.primary.set"
type PrimarySetPayload struct {
Name string `json:"domain"`
}

View File

@@ -0,0 +1,7 @@
package domain
const RemovedTypeSuffix = "domain.removed"
type RemovedPayload struct {
Name string `json:"domain"`
}

View File

@@ -0,0 +1,7 @@
package domain
const VerifiedTypeSuffix = "domain.verified"
type VerifiedPayload struct {
Name string `json:"domain"`
}

View 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
}

View 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
}
}

View 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
}

View 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

View 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,
}
}

View 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
}

View 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)
}
})
}
}

View 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)
)`

File diff suppressed because it is too large Load Diff

View 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")
}
}

File diff suppressed because it is too large Load Diff

View 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)
}

View 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...)
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

View 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,
}
}

View File

@@ -0,0 +1,8 @@
package instance
import "github.com/zitadel/zitadel/internal/repository/instance"
const (
AggregateType = string(instance.AggregateType)
eventTypePrefix = AggregateType + "."
)

View 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
}

View 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
}

View 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
}

View 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,
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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...)
}

View 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))
}
}

View File

@@ -0,0 +1,8 @@
package system
const (
AggregateType = "system"
AggregateOwner = "SYSTEM"
AggregateInstance = ""
EventTypePrefix = AggregateType + "."
)

View 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,
}
}

View File

@@ -0,0 +1,8 @@
package mirror
import "github.com/zitadel/zitadel/internal/v2/system"
const (
Creator = "MIRROR"
eventTypePrefix = system.EventTypePrefix + "mirror."
)

View 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,
},
}
}

View 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,
},
}
}

View 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,
},
},
}
}

View 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,
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}