move files

This commit is contained in:
adlerhurst
2025-05-06 07:18:11 +02:00
parent 050aa7dd48
commit 6ba86bc67b
21 changed files with 916 additions and 976 deletions

View File

@@ -3,7 +3,6 @@ package domain
import (
"context"
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
@@ -20,10 +19,8 @@ var (
func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
cmd := &CreateUserCommand{
user: &User{
User: v4.User{
Username: username,
Traits: &v4.Human{},
},
Traits: &Human{},
},
}

View File

@@ -1,45 +1,45 @@
package domain_test
import (
"context"
"testing"
// import (
// "context"
// "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
// "github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/require"
// "go.opentelemetry.io/otel"
// "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
// sdktrace "go.opentelemetry.io/otel/sdk/trace"
. "github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// . "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// )
func TestExample(t *testing.T) {
ctx := context.Background()
// func TestExample(t *testing.T) {
// ctx := context.Background()
// SetPool(pool)
// // SetPool(pool)
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
require.NoError(t, err)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(exporter),
)
otel.SetTracerProvider(tracerProvider)
SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
// exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
// require.NoError(t, err)
// tracerProvider := sdktrace.NewTracerProvider(
// sdktrace.WithSyncer(exporter),
// )
// otel.SetTracerProvider(tracerProvider)
// SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
// defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
SetUserRepository(repository.User)
SetInstanceRepository(repository.Instance)
SetCryptoRepository(repository.Crypto)
// SetUserRepository(repository.User)
// SetInstanceRepository(repository.Instance)
// SetCryptoRepository(repository.Crypto)
t.Run("verified email", func(t *testing.T) {
err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
assert.NoError(t, err)
})
// t.Run("verified email", func(t *testing.T) {
// err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
// assert.NoError(t, err)
// })
t.Run("unverified email", func(t *testing.T) {
err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
assert.NoError(t, err)
})
}
// t.Run("unverified email", func(t *testing.T) {
// err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
// assert.NoError(t, err)
// })
// }

View File

@@ -14,7 +14,7 @@ func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedComma
return &EmailVerifiedCommand{
UserID: userID,
Email: &Email{
IsVerified: isVerified,
VerifiedAt: time.Time{},
},
}
}

View File

@@ -1,82 +0,0 @@
package domain
import (
"time"
"golang.org/x/exp/constraints"
)
type Operation interface {
// TextOperation |
// NumberOperation |
// BoolOperation
op()
}
type clause[F ~uint8, Op Operation] struct {
field F
op Op
}
func (c *clause[F, Op]) Field() F {
return c.field
}
func (c *clause[F, Op]) Operation() Op {
return c.op
}
type Text interface {
~string | ~[]byte
}
type TextOperation uint8
const (
TextOperationEqual TextOperation = iota
TextOperationNotEqual
TextOperationStartsWith
TextOperationStartsWithIgnoreCase
)
func (TextOperation) op() {}
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time
}
type NumberOperation uint8
const (
NumberOperationEqual NumberOperation = iota
NumberOperationNotEqual
NumberOperationLessThan
NumberOperationLessThanOrEqual
NumberOperationGreaterThan
NumberOperationGreaterThanOrEqual
)
func (NumberOperation) op() {}
type Bool interface {
~bool
}
type BoolOperation uint8
const (
BoolOperationIs BoolOperation = iota
BoolOperationNot
)
func (BoolOperation) op() {}
type ListOperation uint8
const (
ListOperationContains ListOperation = iota
ListOperationNotContains
)
func (ListOperation) op() {}

View File

@@ -4,82 +4,129 @@ import (
"context"
"time"
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type userColumns interface {
// TODO: move v4.columns to domain
InstanceIDColumn() v4.Column
OrgIDColumn() v4.Column
IDColumn() v4.Column
usernameColumn() v4.Column
CreatedAtColumn() v4.Column
UpdatedAtColumn() v4.Column
DeletedAtColumn() v4.Column
// InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() database.Column
// OrgIDColumn returns the column for the org id field.
OrgIDColumn() database.Column
// IDColumn returns the column for the id field.
IDColumn() database.Column
// UsernameColumn returns the column for the username field.
UsernameColumn() database.Column
// CreatedAtColumn returns the column for the created at field.
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the updated at field.
UpdatedAtColumn() database.Column
// DeletedAtColumn returns the column for the deleted at field.
DeletedAtColumn() database.Column
}
type userConditions interface {
InstanceIDCondition(instanceID string) v4.Condition
OrgIDCondition(orgID string) v4.Condition
IDCondition(userID string) v4.Condition
UsernameCondition(op v4.TextOperator, username string) v4.Condition
CreatedAtCondition(op v4.NumberOperator, createdAt time.Time) v4.Condition
UpdatedAtCondition(op v4.NumberOperator, updatedAt time.Time) v4.Condition
DeletedCondition(isDeleted bool) v4.Condition
DeletedAtCondition(op v4.NumberOperator, deletedAt time.Time) v4.Condition
// InstanceIDCondition returns an equal filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
// OrgIDCondition returns an equal filter on the org id field.
OrgIDCondition(orgID string) database.Condition
// IDCondition returns an equal filter on the id field.
IDCondition(userID string) database.Condition
// UsernameCondition returns a filter on the username field.
UsernameCondition(op database.TextOperation, username string) database.Condition
// CreatedAtCondition returns a filter on the created at field.
CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition
// UpdatedAtCondition returns a filter on the updated at field.
UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition
// DeletedAtCondition filters for deleted users is isDeleted is set to true otherwise only not deleted users must be filtered.
DeletedCondition(isDeleted bool) database.Condition
// DeletedAtCondition filters for deleted users based on the given parameters.
DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition
}
type userChanges interface {
SetUsername(username string) v4.Change
// SetUsername sets the username column.
SetUsername(username string) database.Change
}
type UserRepository interface {
userColumns
userConditions
userChanges
// TODO: move condition to domain
Get(ctx context.Context, opts v4.QueryOption) (*User, error)
List(ctx context.Context, opts v4.QueryOption) ([]*User, error)
Delete(ctx context.Context, condition v4.Condition) error
// Get returns a user based on the given condition.
Get(ctx context.Context, opts ...database.QueryOption) (*User, error)
// List returns a list of users based on the given condition.
List(ctx context.Context, opts ...database.QueryOption) ([]*User, error)
// Create creates a new user.
Create(ctx context.Context, user *User) error
// Delete removes users based on the given condition.
Delete(ctx context.Context, condition database.Condition) error
// Human returns the [HumanRepository].
Human() HumanRepository
// Machine returns the [MachineRepository].
Machine() MachineRepository
}
type humanColumns interface {
userColumns
FirstNameColumn() v4.Column
LastNameColumn() v4.Column
EmailAddressColumn() v4.Column
EmailVerifiedAtColumn() v4.Column
PhoneNumberColumn() v4.Column
PhoneVerifiedAtColumn() v4.Column
// FirstNameColumn returns the column for the first name field.
FirstNameColumn() database.Column
// LastNameColumn returns the column for the last name field.
LastNameColumn() database.Column
// EmailAddressColumn returns the column for the email address field.
EmailAddressColumn() database.Column
// EmailVerifiedAtColumn returns the column for the email verified at field.
EmailVerifiedAtColumn() database.Column
// PhoneNumberColumn returns the column for the phone number field.
PhoneNumberColumn() database.Column
// PhoneVerifiedAtColumn returns the column for the phone verified at field.
PhoneVerifiedAtColumn() database.Column
}
type humanConditions interface {
userConditions
FirstNameCondition(op v4.TextOperator, firstName string) v4.Condition
LastNameCondition(op v4.TextOperator, lastName string) v4.Condition
EmailAddressCondition(op v4.TextOperator, email string) v4.Condition
EmailAddressVerifiedCondition(isVerified bool) v4.Condition
EmailVerifiedAtCondition(op v4.TextOperator, emailVerifiedAt string) v4.Condition
PhoneNumberCondition(op v4.TextOperator, phoneNumber string) v4.Condition
PhoneNumberVerifiedCondition(isVerified bool) v4.Condition
PhoneVerifiedAtCondition(op v4.TextOperator, phoneVerifiedAt string) v4.Condition
// FirstNameCondition returns a filter on the first name field.
FirstNameCondition(op database.TextOperation, firstName string) database.Condition
// LastNameCondition returns a filter on the last name field.
LastNameCondition(op database.TextOperation, lastName string) database.Condition
// EmailAddressCondition returns a filter on the email address field.
EmailAddressCondition(op database.TextOperation, email string) database.Condition
// EmailVerifiedCondition returns a filter that checks if the email is verified or not.
EmailVerifiedCondition(isVerified bool) database.Condition
// EmailVerifiedAtCondition returns a filter on the email verified at field.
EmailVerifiedAtCondition(op database.NumberOperation, emailVerifiedAt time.Time) database.Condition
// PhoneNumberCondition returns a filter on the phone number field.
PhoneNumberCondition(op database.TextOperation, phoneNumber string) database.Condition
// PhoneVerifiedCondition returns a filter that checks if the phone is verified or not.
PhoneVerifiedCondition(isVerified bool) database.Condition
// PhoneVerifiedAtCondition returns a filter on the phone verified at field.
PhoneVerifiedAtCondition(op database.NumberOperation, phoneVerifiedAt time.Time) database.Condition
}
type humanChanges interface {
userChanges
SetFirstName(firstName string) v4.Change
SetLastName(lastName string) v4.Change
// SetFirstName sets the first name field of the human.
SetFirstName(firstName string) database.Change
// SetLastName sets the last name field of the human.
SetLastName(lastName string) database.Change
SetEmail(address string, verified *time.Time) v4.Change
SetEmailAddress(email string) v4.Change
SetEmailVerifiedAt(emailVerifiedAt time.Time) v4.Change
// SetEmail sets the email address and verified field of the email
// if verifiedAt is nil the email is not verified
SetEmail(address string, verifiedAt *time.Time) database.Change
// SetEmailAddress sets the email address field of the email
SetEmailAddress(email string) database.Change
// SetEmailVerifiedAt sets the verified column of the email
// if at is zero the statement uses the database timestamp
SetEmailVerifiedAt(at time.Time) database.Change
SetPhone(number string, verifiedAt *time.Time) v4.Change
SetPhoneNumber(phoneNumber string) v4.Change
SetPhoneVerifiedAt(phoneVerifiedAt time.Time) v4.Change
// SetPhone sets the phone number and verified field
// if verifiedAt is nil the phone is not verified
SetPhone(number string, verifiedAt *time.Time) database.Change
// SetPhoneNumber sets the phone number field
SetPhoneNumber(phoneNumber string) database.Change
// SetPhoneVerifiedAt sets the verified field of the phone
// if at is zero the statement uses the database timestamp
SetPhoneVerifiedAt(at time.Time) database.Change
}
type HumanRepository interface {
@@ -87,144 +134,95 @@ type HumanRepository interface {
humanConditions
humanChanges
GetEmail(ctx context.Context, condition v4.Condition) (*Email, error)
// TODO: replace any with add email update columns
Create(ctx context.Context, user *User) error
Update(ctx context.Context, condition v4.Condition, changes ...v4.Change) error
// Get returns an email based on the given condition.
GetEmail(ctx context.Context, condition database.Condition) (*Email, error)
// Update updates human users based on the given condition and changes.
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
}
type machineColumns interface {
userColumns
DescriptionColumn() v4.Column
// DescriptionColumn returns the column for the description field.
DescriptionColumn() database.Column
}
type machineConditions interface {
userConditions
DescriptionCondition(op v4.TextOperator, description string) v4.Condition
// DescriptionCondition returns a filter on the description field.
DescriptionCondition(op database.TextOperation, description string) database.Condition
}
type machineChanges interface {
userChanges
SetDescription(description string) v4.Change
// SetDescription sets the description field of the machine.
SetDescription(description string) database.Change
}
type MachineRepository interface {
// Update updates machine users based on the given condition and changes.
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
machineColumns
machineConditions
machineChanges
Create(ctx context.Context, user *User) error
Update(ctx context.Context, condition v4.Condition, changes ...v4.Change) error
}
// type UserRepository interface {
// // Get(ctx context.Context, clauses ...UserClause) (*User, error)
// // Search(ctx context.Context, clauses ...UserClause) ([]*User, error)
type UserTraits interface {
Type() UserType
}
// UserQuery[UserOperation]
// Human() HumanQuery
// Machine() MachineQuery
// }
type UserType string
// type UserQuery[Op UserOperation] interface {
// ByID(id string) UserQuery[Op]
// Username(username string) UserQuery[Op]
// Exec() Op
// }
// type HumanQuery interface {
// UserQuery[HumanOperation]
// Email(op TextOperation, email string) HumanQuery
// HumanOperation
// }
// type MachineQuery interface {
// UserQuery[MachineOperation]
// MachineOperation
// }
// type UserClause interface {
// Field() UserField
// Operation() Operation
// Args() []any
// }
// type UserField uint8
// const (
// // Fields used for all users
// UserFieldInstanceID UserField = iota + 1
// UserFieldOrgID
// UserFieldID
// UserFieldUsername
// // Fields used for human users
// UserHumanFieldEmail
// UserHumanFieldEmailVerified
// // Fields used for machine users
// UserMachineFieldDescription
// )
// type userByIDClause struct {
// id string
// }
// func (c *userByIDClause) Field() UserField {
// return UserFieldID
// }
// func (c *userByIDClause) Operation() Operation {
// return TextOperationEqual
// }
// func (c *userByIDClause) Args() []any {
// return []any{c.id}
// }
// type UserOperation interface {
// Delete(ctx context.Context) error
// SetUsername(ctx context.Context, username string) error
// }
// type HumanOperation interface {
// UserOperation
// SetEmail(ctx context.Context, email string) error
// SetEmailVerified(ctx context.Context, email string) error
// GetEmail(ctx context.Context) (*Email, error)
// }
// type MachineOperation interface {
// UserOperation
// SetDescription(ctx context.Context, description string) error
// }
const (
UserTypeHuman UserType = "human"
UserTypeMachine UserType = "machine"
)
type User struct {
v4.User
InstanceID string
OrgID string
ID string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
Username string
Traits UserTraits
}
type Human struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email *Email `json:"email,omitempty"`
Phone *Phone `json:"phone,omitempty"`
}
// Type implements [UserTraits].
func (h *Human) Type() UserType {
return UserTypeHuman
}
var _ UserTraits = (*Human)(nil)
type Email struct {
v4.Email
IsVerified bool
Address string `json:"address"`
VerifiedAt time.Time `json:"verifiedAt"`
}
// type userTraits interface {
// isUserTraits()
// }
type Phone struct {
Number string `json:"number"`
VerifiedAt time.Time `json:"verifiedAt"`
}
// type Human struct {
// Email *Email `json:"email"`
// }
type Machine struct {
Description string `json:"description"`
}
// func (*Human) isUserTraits() {}
// Type implements [UserTraits].
func (m *Machine) Type() UserType {
return UserTypeMachine
}
// type Machine struct {
// Description string `json:"description"`
// }
// func (*Machine) isUserTraits() {}
// type Email struct {
// Address string `json:"address"`
// IsVerified bool `json:"isVerified"`
// }
var _ UserTraits = (*Machine)(nil)

View File

@@ -0,0 +1,51 @@
package database
type Change interface {
Write(builder *StatementBuilder)
}
type change[V Value] struct {
column Column
value V
}
var _ Change = (*change[string])(nil)
func NewChange[V Value](col Column, value V) Change {
return &change[V]{
column: col,
value: value,
}
}
func NewChangePtr[V Value](col Column, value *V) Change {
if value == nil {
return NewChange(col, NullInstruction)
}
return NewChange(col, *value)
}
// Write implements [Change].
func (c change[V]) Write(builder *StatementBuilder) {
c.column.Write(builder)
builder.WriteString(" = ")
builder.WriteArg(c.value)
}
type Changes []Change
func NewChanges(cols ...Change) Change {
return Changes(cols)
}
// Write implements [Change].
func (m Changes) Write(builder *StatementBuilder) {
for i, col := range m {
if i > 0 {
builder.WriteString(", ")
}
col.Write(builder)
}
}
var _ Change = Changes(nil)

View File

@@ -0,0 +1,55 @@
package database
type Columns []Column
// Write implements [Column].
func (m Columns) Write(builder *StatementBuilder) {
for i, col := range m {
if i > 0 {
builder.WriteString(", ")
}
col.Write(builder)
}
}
type Column interface {
Write(builder *StatementBuilder)
}
type column struct {
name string
}
func NewColumn(name string) Column {
return column{name: name}
}
// Write implements [Column].
func (c column) Write(builder *StatementBuilder) {
builder.WriteString(c.name)
}
var _ Column = (*column)(nil)
type ignoreCaseColumn interface {
Column
WriteIgnoreCase(builder *StatementBuilder)
}
func NewIgnoreCaseColumn(name, suffix string) ignoreCaseColumn {
return ignoreCaseCol{
column: column{name: name},
suffix: suffix,
}
}
type ignoreCaseCol struct {
column
suffix string
}
// WriteIgnoreCase implements [ignoreCaseColumn].
func (c ignoreCaseCol) WriteIgnoreCase(builder *StatementBuilder) {
c.column.Write(builder)
builder.WriteString(c.suffix)
}

View File

@@ -1,15 +1,15 @@
package v4
package database
type Condition interface {
writeTo(builder *statementBuilder)
Write(builder *StatementBuilder)
}
type and struct {
conditions []Condition
}
// writeTo implements [Condition].
func (a *and) writeTo(builder *statementBuilder) {
// Write implements [Condition].
func (a *and) Write(builder *StatementBuilder) {
if len(a.conditions) > 1 {
builder.WriteString("(")
defer builder.WriteString(")")
@@ -18,7 +18,7 @@ func (a *and) writeTo(builder *statementBuilder) {
if i > 0 {
builder.WriteString(" AND ")
}
condition.writeTo(builder)
condition.(Condition).Write(builder)
}
}
@@ -32,8 +32,8 @@ type or struct {
conditions []Condition
}
// writeTo implements [Condition].
func (o *or) writeTo(builder *statementBuilder) {
// Write implements [Condition].
func (o *or) Write(builder *StatementBuilder) {
if len(o.conditions) > 1 {
builder.WriteString("(")
defer builder.WriteString(")")
@@ -42,7 +42,7 @@ func (o *or) writeTo(builder *statementBuilder) {
if i > 0 {
builder.WriteString(" OR ")
}
condition.writeTo(builder)
condition.(Condition).Write(builder)
}
}
@@ -56,9 +56,9 @@ type isNull struct {
column Column
}
// writeTo implements [Condition].
func (i *isNull) writeTo(builder *statementBuilder) {
i.column.writeTo(builder)
// Write implements [Condition].
func (i *isNull) Write(builder *StatementBuilder) {
i.column.Write(builder)
builder.WriteString(" IS NULL")
}
@@ -72,40 +72,40 @@ type isNotNull struct {
column Column
}
// writeTo implements [Condition].
func (i *isNotNull) writeTo(builder *statementBuilder) {
i.column.writeTo(builder)
// Write implements [Condition].
func (i *isNotNull) Write(builder *StatementBuilder) {
i.column.Write(builder)
builder.WriteString(" IS NOT NULL")
}
func IsNotNull(column Column) *isNotNull {
return &isNotNull{column: column}
return &isNotNull{column: column.(Column)}
}
var _ Condition = (*isNotNull)(nil)
type valueCondition func(builder *statementBuilder)
type valueCondition func(builder *StatementBuilder)
func newTextCondition[V Text](col Column, op TextOperator, value V) Condition {
return valueCondition(func(builder *statementBuilder) {
func NewTextCondition[V Text](col Column, op TextOperation, value V) Condition {
return valueCondition(func(builder *StatementBuilder) {
writeTextOperation(builder, col, op, value)
})
}
func newNumberCondition[V Number](col Column, op NumberOperator, value V) Condition {
return valueCondition(func(builder *statementBuilder) {
func NewNumberCondition[V Number](col Column, op NumberOperation, value V) Condition {
return valueCondition(func(builder *StatementBuilder) {
writeNumberOperation(builder, col, op, value)
})
}
func newBooleanCondition[V Boolean](col Column, value V) Condition {
return valueCondition(func(builder *statementBuilder) {
func NewBooleanCondition[V Boolean](col Column, value V) Condition {
return valueCondition(func(builder *StatementBuilder) {
writeBooleanOperation(builder, col, value)
})
}
// writeTo implements [Condition].
func (c valueCondition) writeTo(builder *statementBuilder) {
// Write implements [Condition].
func (c valueCondition) Write(builder *StatementBuilder) {
c(builder)
}

View File

@@ -0,0 +1,139 @@
package database
import (
"time"
"golang.org/x/exp/constraints"
)
type Value interface {
Boolean | Number | Text | Instruction
}
type Operation interface {
BooleanOperation | NumberOperation | TextOperation
}
type Text interface {
~string | ~[]byte
}
type TextOperation uint8
const (
// TextOperationEqual compares two strings for equality.
TextOperationEqual TextOperation = iota + 1
// TextOperationEqualIgnoreCase compares two strings for equality, ignoring case.
TextOperationEqualIgnoreCase
// TextOperationNotEqual compares two strings for inequality.
TextOperationNotEqual
// TextOperationNotEqualIgnoreCase compares two strings for inequality, ignoring case.
TextOperationNotEqualIgnoreCase
// TextOperationStartsWith checks if the first string starts with the second.
TextOperationStartsWith
// TextOperationStartsWithIgnoreCase checks if the first string starts with the second, ignoring case.
TextOperationStartsWithIgnoreCase
)
var textOperations = map[TextOperation]string{
TextOperationEqual: " = ",
TextOperationEqualIgnoreCase: " LIKE ",
TextOperationNotEqual: " <> ",
TextOperationNotEqualIgnoreCase: " NOT LIKE ",
TextOperationStartsWith: " LIKE ",
TextOperationStartsWithIgnoreCase: " LIKE ",
}
func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOperation, value T) {
switch op {
case TextOperationEqual, TextOperationNotEqual:
col.Write(builder)
builder.WriteString(textOperations[op])
builder.WriteString(builder.AppendArg(value))
case TextOperationEqualIgnoreCase, TextOperationNotEqualIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
ignoreCaseCol.WriteIgnoreCase(builder)
} else {
builder.WriteString("LOWER(")
col.Write(builder)
builder.WriteString(")")
}
builder.WriteString(textOperations[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.AppendArg(value))
builder.WriteString(")")
case TextOperationStartsWith:
col.Write(builder)
builder.WriteString(textOperations[op])
builder.WriteString(builder.AppendArg(value))
builder.WriteString(" || '%'")
case TextOperationStartsWithIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
ignoreCaseCol.WriteIgnoreCase(builder)
} else {
builder.WriteString("LOWER(")
col.Write(builder)
builder.WriteString(")")
}
builder.WriteString(textOperations[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.AppendArg(value))
builder.WriteString(")")
builder.WriteString(" || '%'")
default:
panic("unsupported text operation")
}
}
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration
}
type NumberOperation uint8
const (
// NumberOperationEqual compares two numbers for equality.
NumberOperationEqual NumberOperation = iota + 1
// NumberOperationNotEqual compares two numbers for inequality.
NumberOperationNotEqual
// NumberOperationLessThan compares two numbers to check if the first is less than the second.
NumberOperationLessThan
// NumberOperationLessThanOrEqual compares two numbers to check if the first is less than or equal to the second.
NumberOperationAtLeast
// NumberOperationGreaterThan compares two numbers to check if the first is greater than the second.
NumberOperationGreaterThan
// NumberOperationGreaterThanOrEqual compares two numbers to check if the first is greater than or equal to the second.
NumberOperationAtMost
)
var numberOperations = map[NumberOperation]string{
NumberOperationEqual: " = ",
NumberOperationNotEqual: " <> ",
NumberOperationLessThan: " < ",
NumberOperationAtLeast: " <= ",
NumberOperationGreaterThan: " > ",
NumberOperationAtMost: " >= ",
}
func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value T) {
col.Write(builder)
builder.WriteString(numberOperations[op])
builder.WriteString(builder.AppendArg(value))
}
type Boolean interface {
~bool
}
type BooleanOperation uint8
const (
BooleanOperationIsTrue BooleanOperation = iota + 1
BooleanOperationIsFalse
)
func writeBooleanOperation[T Boolean](builder *StatementBuilder, col Column, value T) {
col.Write(builder)
builder.WriteString(" IS ")
builder.WriteString(builder.AppendArg(value))
}

View File

@@ -0,0 +1,66 @@
package database
type QueryOption func(opts *QueryOpts)
func WithCondition(condition Condition) QueryOption {
return func(opts *QueryOpts) {
opts.Condition = condition
}
}
func WithOrderBy(orderBy ...Column) QueryOption {
return func(opts *QueryOpts) {
opts.OrderBy = orderBy
}
}
func WithLimit(limit uint32) QueryOption {
return func(opts *QueryOpts) {
opts.Limit = limit
}
}
func WithOffset(offset uint32) QueryOption {
return func(opts *QueryOpts) {
opts.Offset = offset
}
}
type QueryOpts struct {
Condition Condition
OrderBy Columns
Limit uint32
Offset uint32
}
func (opts *QueryOpts) WriteCondition(builder *StatementBuilder) {
if opts.Condition == nil {
return
}
builder.WriteString(" WHERE ")
opts.Condition.Write(builder)
}
func (opts *QueryOpts) WriteOrderBy(builder *StatementBuilder) {
if len(opts.OrderBy) == 0 {
return
}
builder.WriteString(" ORDER BY ")
Columns(opts.OrderBy).Write(builder)
}
func (opts *QueryOpts) WriteLimit(builder *StatementBuilder) {
if opts.Limit == 0 {
return
}
builder.WriteString(" LIMIT ")
builder.WriteArg(opts.Limit)
}
func (opts *QueryOpts) WriteOffset(builder *StatementBuilder) {
if opts.Offset == 0 {
return
}
builder.WriteString(" OFFSET ")
builder.WriteArg(opts.Offset)
}

View File

@@ -1,89 +0,0 @@
package v4
type Change interface {
Column
}
type change[V Value] struct {
column Column
value V
}
func newChange[V Value](col Column, value V) Change {
return &change[V]{
column: col,
value: value,
}
}
func newUpdatePtrColumn[V Value](col Column, value *V) Change {
if value == nil {
return newChange(col, nullDBInstruction)
}
return newChange(col, *value)
}
// writeTo implements [Change].
func (c change[V]) writeTo(builder *statementBuilder) {
c.column.writeTo(builder)
builder.WriteString(" = ")
builder.writeArg(c.value)
}
type Changes []Change
func newChanges(cols ...Change) Change {
return Changes(cols)
}
// writeTo implements [Change].
func (m Changes) writeTo(builder *statementBuilder) {
for i, col := range m {
if i > 0 {
builder.WriteString(", ")
}
col.writeTo(builder)
}
}
var _ Change = Changes(nil)
var _ Change = (*change[string])(nil)
type Columns []Column
func (m Columns) writeTo(builder *statementBuilder) {
for i, col := range m {
if i > 0 {
builder.WriteString(", ")
}
col.writeTo(builder)
}
}
type Column interface {
writeTo(builder *statementBuilder)
}
type column struct {
name string
}
func (c column) writeTo(builder *statementBuilder) {
builder.WriteString(c.name)
}
type ignoreCaseColumn interface {
Column
writeIgnoreCaseTo(builder *statementBuilder)
}
type ignoreCaseCol struct {
column
suffix string
}
func (c ignoreCaseCol) writeIgnoreCaseTo(builder *statementBuilder) {
c.column.writeTo(builder)
builder.WriteString(c.suffix)
}

View File

@@ -1,139 +0,0 @@
package v4
import (
"time"
"golang.org/x/exp/constraints"
)
type Value interface {
Boolean | Number | Text | databaseInstruction
}
type Operator interface {
BooleanOperator | NumberOperator | TextOperator
}
type Text interface {
~string | ~[]byte
}
type TextOperator uint8
const (
// TextOperatorEqual compares two strings for equality.
TextOperatorEqual TextOperator = iota + 1
// TextOperatorEqualIgnoreCase compares two strings for equality, ignoring case.
TextOperatorEqualIgnoreCase
// TextOperatorNotEqual compares two strings for inequality.
TextOperatorNotEqual
// TextOperatorNotEqualIgnoreCase compares two strings for inequality, ignoring case.
TextOperatorNotEqualIgnoreCase
// TextOperatorStartsWith checks if the first string starts with the second.
TextOperatorStartsWith
// TextOperatorStartsWithIgnoreCase checks if the first string starts with the second, ignoring case.
TextOperatorStartsWithIgnoreCase
)
var textOperators = map[TextOperator]string{
TextOperatorEqual: " = ",
TextOperatorEqualIgnoreCase: " LIKE ",
TextOperatorNotEqual: " <> ",
TextOperatorNotEqualIgnoreCase: " NOT LIKE ",
TextOperatorStartsWith: " LIKE ",
TextOperatorStartsWithIgnoreCase: " LIKE ",
}
func writeTextOperation[T Text](builder *statementBuilder, col Column, op TextOperator, value T) {
switch op {
case TextOperatorEqual, TextOperatorNotEqual:
col.writeTo(builder)
builder.WriteString(textOperators[op])
builder.WriteString(builder.appendArg(value))
case TextOperatorEqualIgnoreCase, TextOperatorNotEqualIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
ignoreCaseCol.writeIgnoreCaseTo(builder)
} else {
builder.WriteString("LOWER(")
col.writeTo(builder)
builder.WriteString(")")
}
builder.WriteString(textOperators[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.appendArg(value))
builder.WriteString(")")
case TextOperatorStartsWith:
col.writeTo(builder)
builder.WriteString(textOperators[op])
builder.WriteString(builder.appendArg(value))
builder.WriteString(" || '%'")
case TextOperatorStartsWithIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
ignoreCaseCol.writeIgnoreCaseTo(builder)
} else {
builder.WriteString("LOWER(")
col.writeTo(builder)
builder.WriteString(")")
}
builder.WriteString(textOperators[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.appendArg(value))
builder.WriteString(")")
builder.WriteString(" || '%'")
default:
panic("unsupported text operation")
}
}
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration
}
type NumberOperator uint8
const (
// NumberOperatorEqual compares two numbers for equality.
NumberOperatorEqual NumberOperator = iota + 1
// NumberOperatorNotEqual compares two numbers for inequality.
NumberOperatorNotEqual
// NumberOperatorLessThan compares two numbers to check if the first is less than the second.
NumberOperatorLessThan
// NumberOperatorLessThanOrEqual compares two numbers to check if the first is less than or equal to the second.
NumberOperatorAtLeast
// NumberOperatorGreaterThan compares two numbers to check if the first is greater than the second.
NumberOperatorGreaterThan
// NumberOperatorGreaterThanOrEqual compares two numbers to check if the first is greater than or equal to the second.
NumberOperatorAtMost
)
var numberOperators = map[NumberOperator]string{
NumberOperatorEqual: " = ",
NumberOperatorNotEqual: " <> ",
NumberOperatorLessThan: " < ",
NumberOperatorAtLeast: " <= ",
NumberOperatorGreaterThan: " > ",
NumberOperatorAtMost: " >= ",
}
func writeNumberOperation[T Number](builder *statementBuilder, col Column, op NumberOperator, value T) {
col.writeTo(builder)
builder.WriteString(numberOperators[op])
builder.WriteString(builder.appendArg(value))
}
type Boolean interface {
~bool
}
type BooleanOperator uint8
const (
BooleanOperatorIsTrue BooleanOperator = iota + 1
BooleanOperatorIsFalse
)
func writeBooleanOperation[T Boolean](builder *statementBuilder, col Column, value T) {
col.writeTo(builder)
builder.WriteString(" IS ")
builder.WriteString(builder.appendArg(value))
}

View File

@@ -4,7 +4,6 @@ type Org struct {
InstanceID string
ID string
Name string
Dates
}
type GetOrg struct{}

View File

@@ -1,66 +0,0 @@
package v4
type queryOpts struct {
condition Condition
orderBy Columns
limit uint32
offset uint32
}
func (opts *queryOpts) writeCondition(builder *statementBuilder) {
if opts.condition == nil {
return
}
builder.WriteString(" WHERE ")
opts.condition.writeTo(builder)
}
func (opts *queryOpts) writeOrderBy(builder *statementBuilder) {
if len(opts.orderBy) == 0 {
return
}
builder.WriteString(" ORDER BY ")
opts.orderBy.writeTo(builder)
}
func (opts *queryOpts) writeLimit(builder *statementBuilder) {
if opts.limit == 0 {
return
}
builder.WriteString(" LIMIT ")
builder.writeArg(opts.limit)
}
func (opts *queryOpts) writeOffset(builder *statementBuilder) {
if opts.offset == 0 {
return
}
builder.WriteString(" OFFSET ")
builder.writeArg(opts.offset)
}
type QueryOption func(*queryOpts)
func WithCondition(condition Condition) QueryOption {
return func(opts *queryOpts) {
opts.condition = condition
}
}
func WithOrderBy(orderBy ...Column) QueryOption {
return func(opts *queryOpts) {
opts.orderBy = orderBy
}
}
func WithLimit(limit uint32) QueryOption {
return func(opts *queryOpts) {
opts.limit = limit
}
}
func WithOffset(offset uint32) QueryOption {
return func(opts *queryOpts) {
opts.offset = offset
}
}

View File

@@ -1,46 +0,0 @@
package v4
import (
"strconv"
"strings"
)
type databaseInstruction string
const (
nowDBInstruction databaseInstruction = "NOW()"
nullDBInstruction databaseInstruction = "NULL"
)
type statementBuilder struct {
strings.Builder
args []any
existingArgs map[any]string
}
func (b *statementBuilder) writeArg(arg any) {
b.WriteString(b.appendArg(arg))
}
func (b *statementBuilder) appendArg(arg any) (placeholder string) {
if b.existingArgs == nil {
b.existingArgs = make(map[any]string)
}
if placeholder, ok := b.existingArgs[arg]; ok {
return placeholder
}
if instruction, ok := arg.(databaseInstruction); ok {
return string(instruction)
}
b.args = append(b.args, arg)
placeholder = "$" + strconv.Itoa(len(b.args))
b.existingArgs[arg] = placeholder
return placeholder
}
func (b *statementBuilder) appendArgs(args ...any) {
for _, arg := range args {
b.appendArg(arg)
}
}

View File

@@ -4,59 +4,55 @@ import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Dates struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}
type User struct {
InstanceID string
OrgID string
ID string
Username string
Traits userTrait
Dates
}
type UserType string
type userTrait interface {
userTrait()
Type() UserType
}
const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` +
` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
` FROM users_view`
type user struct {
builder statementBuilder
builder database.StatementBuilder
client database.QueryExecutor
}
func UserRepository(client database.QueryExecutor) *user {
func UserRepository(client database.QueryExecutor) domain.UserRepository {
return &user{
client: client,
}
}
func (u *user) List(ctx context.Context, opts ...QueryOption) (users []*User, err error) {
options := new(queryOpts)
var _ domain.UserRepository = (*user)(nil)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
// Human implements [domain.UserRepository].
func (u *user) Human() domain.HumanRepository {
return &userHuman{user: u}
}
// Machine implements [domain.UserRepository].
func (u *user) Machine() domain.MachineRepository {
return &userMachine{user: u}
}
// List implements [domain.UserRepository].
func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder)
options.writeOrderBy(&u.builder)
options.writeLimit(&u.builder)
options.writeOffset(&u.builder)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
rows, err := u.client.Query(ctx, u.builder.String(), u.builder.args...)
rows, err := u.client.Query(ctx, u.builder.String(), u.builder.Args()...)
if err != nil {
return nil, err
}
@@ -80,142 +76,157 @@ func (u *user) List(ctx context.Context, opts ...QueryOption) (users []*User, er
return users, nil
}
func (u *user) Get(ctx context.Context, opts ...QueryOption) (*User, error) {
options := new(queryOpts)
// Get implements [domain.UserRepository].
func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder)
options.writeOrderBy(&u.builder)
options.writeLimit(&u.builder)
options.writeOffset(&u.builder)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.args...))
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...))
}
const (
// TODO: change to separate statements and tables
createUserCte = `WITH user AS (` +
`INSERT INTO users (instance_id, org_id, id, username, type) VALUES ($1, $2, $3, $4, $5)` +
` RETURNING *)`
createHumanStmt = createUserCte + ` INSERT INTO user_humans h (instance_id, org_id, user_id, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` +
` SELECT u.instance_id, u.org_id, u.id, $6, $7, $8, $9, $10, $11` +
` FROM user u` +
` RETURNING u.created_at, u.updated_at, u.deleted_at`
createMachineStmt = createUserCte + ` INSERT INTO user_machines (instance_id, org_id, user_id, description)` +
` SELECT u.instance_id, u.org_id, u.id, $6` +
` FROM user u` +
` RETURNING u.created_at, u.updated_at`
createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` +
` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` +
` RETURNING created_at, updated_at`
createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` +
` VALUES ($1, $2, $3, $4, $5)` +
` RETURNING created_at, updated_at`
)
func (u *user) Create(ctx context.Context, user *User) error {
u.builder.appendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type())
// Create implements [domain.UserRepository].
func (u *user) Create(ctx context.Context, user *domain.User) error {
u.builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type())
switch trait := user.Traits.(type) {
case *Human:
case *domain.Human:
u.builder.WriteString(createHumanStmt)
u.builder.appendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
case *Machine:
u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
case *domain.Machine:
u.builder.WriteString(createMachineStmt)
u.builder.appendArgs(trait.Description)
u.builder.AppendArgs(trait.Description)
}
return u.client.QueryRow(ctx, u.builder.String(), u.builder.args...).Scan(&user.Dates.CreatedAt, &user.Dates.UpdatedAt)
return u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
}
func (u *user) Update(ctx context.Context, condition Condition, changes ...Change) error {
u.builder.WriteString("UPDATE users SET ")
Changes(changes).writeTo(&u.builder)
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.args...)
}
func (u *user) Delete(ctx context.Context, condition Condition) error {
// Delete implements [domain.UserRepository].
func (u *user) Delete(ctx context.Context, condition database.Condition) error {
u.builder.WriteString("DELETE FROM users")
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.args...)
return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...)
}
func (u *user) InstanceIDColumn() Column {
return column{name: "instance_id"}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetUsername implements [domain.userChanges].
func (u user) SetUsername(username string) database.Change {
return database.NewChange(u.UsernameColumn(), username)
}
func (u *user) InstanceIDCondition(instanceID string) Condition {
return newTextCondition(u.InstanceIDColumn(), TextOperatorEqual, instanceID)
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// InstanceIDCondition implements [domain.userConditions].
func (u user) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
func (u *user) OrgIDColumn() Column {
return column{name: "org_id"}
// OrgIDCondition implements [domain.userConditions].
func (u user) OrgIDCondition(orgID string) database.Condition {
return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID)
}
func (u *user) OrgIDCondition(orgID string) Condition {
return newTextCondition(u.OrgIDColumn(), TextOperatorEqual, orgID)
// IDCondition implements [domain.userConditions].
func (u user) IDCondition(userID string) database.Condition {
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
}
func (u *user) IDColumn() Column {
return column{name: "id"}
// UsernameCondition implements [domain.userConditions].
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
return database.NewTextCondition(u.UsernameColumn(), op, username)
}
func (u *user) IDCondition(userID string) Condition {
return newTextCondition(u.IDColumn(), TextOperatorEqual, userID)
// CreatedAtCondition implements [domain.userConditions].
func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt)
}
func (u *user) UsernameColumn() Column {
return ignoreCaseCol{
column: column{name: "username"},
suffix: "_lower",
}
// UpdatedAtCondition implements [domain.userConditions].
func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
}
func (u user) SetUsername(username string) Change {
return newChange(u.UsernameColumn(), username)
}
func (u *user) UsernameCondition(op TextOperator, username string) Condition {
return newTextCondition(u.UsernameColumn(), op, username)
}
func (u *user) CreatedAtColumn() Column {
return column{name: "created_at"}
}
func (u *user) CreatedAtCondition(op NumberOperator, createdAt time.Time) Condition {
return newNumberCondition(u.CreatedAtColumn(), op, createdAt)
}
func (u *user) UpdatedAtColumn() Column {
return column{name: "updated_at"}
}
func (u *user) UpdatedAtCondition(op NumberOperator, updatedAt time.Time) Condition {
return newNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
}
func (u *user) DeletedAtColumn() Column {
return column{name: "deleted_at"}
}
func (u *user) DeletedCondition(isDeleted bool) Condition {
// DeletedCondition implements [domain.userConditions].
func (u user) DeletedCondition(isDeleted bool) database.Condition {
if isDeleted {
return IsNotNull(u.DeletedAtColumn())
return database.IsNotNull(u.DeletedAtColumn())
}
return IsNull(u.DeletedAtColumn())
return database.IsNull(u.DeletedAtColumn())
}
func (u *user) DeletedAtCondition(op NumberOperator, deletedAt time.Time) Condition {
return newNumberCondition(u.DeletedAtColumn(), op, deletedAt)
// DeletedAtCondition implements [domain.userConditions].
func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition {
return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt)
}
func (u *user) writeCondition(condition Condition) {
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// InstanceIDColumn implements [domain.userColumns].
func (user) InstanceIDColumn() database.Column {
return database.NewColumn("instance_id")
}
// OrgIDColumn implements [domain.userColumns].
func (user) OrgIDColumn() database.Column {
return database.NewColumn("org_id")
}
// IDColumn implements [domain.userColumns].
func (user) IDColumn() database.Column {
return database.NewColumn("id")
}
// UsernameColumn implements [domain.userColumns].
func (user) UsernameColumn() database.Column {
return database.NewIgnoreCaseColumn("username", "_lower")
}
// FirstNameColumn implements [domain.userColumns].
func (user) CreatedAtColumn() database.Column {
return database.NewColumn("created_at")
}
// UpdatedAtColumn implements [domain.userColumns].
func (user) UpdatedAtColumn() database.Column {
return database.NewColumn("updated_at")
}
// DeletedAtColumn implements [domain.userColumns].
func (user) DeletedAtColumn() database.Column {
return database.NewColumn("deleted_at")
}
func (u *user) writeCondition(condition database.Condition) {
if condition == nil {
return
}
u.builder.WriteString(" WHERE ")
condition.writeTo(&u.builder)
condition.Write(&u.builder)
}
func (u user) columns() Columns {
return Columns{
func (u user) columns() database.Columns {
return database.Columns{
u.InstanceIDColumn(),
u.OrgIDColumn(),
u.IDColumn(),
@@ -226,14 +237,14 @@ func (u user) columns() Columns {
}
}
func scanUser(scanner database.Scanner) (*User, error) {
func scanUser(scanner database.Scanner) (*domain.User, error) {
var (
user User
human Human
email Email
phone Phone
machine Machine
typ UserType
user domain.User
human domain.Human
email domain.Email
phone domain.Phone
machine domain.Machine
typ domain.UserType
)
err := scanner.Scan(
&user.InstanceID,
@@ -241,9 +252,9 @@ func scanUser(scanner database.Scanner) (*User, error) {
&user.ID,
&user.Username,
&typ,
&user.Dates.CreatedAt,
&user.Dates.UpdatedAt,
&user.Dates.DeletedAt,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
&human.FirstName,
&human.LastName,
&email.Address,
@@ -257,7 +268,7 @@ func scanUser(scanner database.Scanner) (*User, error) {
}
switch typ {
case UserTypeHuman:
case domain.UserTypeHuman:
if email.Address != "" {
human.Email = &email
}
@@ -265,7 +276,7 @@ func scanUser(scanner database.Scanner) (*User, error) {
human.Phone = &phone
}
user.Traits = &human
case UserTypeMachine:
case domain.UserTypeMachine:
user.Traits = &machine
}

View File

@@ -3,58 +3,33 @@ package v4
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Human struct {
FirstName string
LastName string
Email *Email
Phone *Phone
}
const UserTypeHuman UserType = "human"
func (Human) userTrait() {}
func (h Human) Type() UserType {
return UserTypeHuman
}
var _ userTrait = (*Human)(nil)
type Email struct {
Address string
Verification
}
type Phone struct {
Number string
Verification
}
type Verification struct {
VerifiedAt time.Time
}
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
type userHuman struct {
*user
}
func (u *user) Human() *userHuman {
return &userHuman{user: u}
}
var _ domain.HumanRepository = (*userHuman)(nil)
const userEmailQuery = `SELECT h.email_address, h.email_verified_at FROM user_humans h`
func (u *userHuman) GetEmail(ctx context.Context, condition Condition) (*Email, error) {
var email Email
// GetEmail implements [domain.HumanRepository].
func (u *userHuman) GetEmail(ctx context.Context, condition database.Condition) (*domain.Email, error) {
var email domain.Email
u.builder.WriteString(userEmailQuery)
u.writeCondition(condition)
err := u.client.QueryRow(ctx, u.builder.String(), u.builder.args...).Scan(
err := u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(
&email.Address,
&email.Verification.VerifiedAt,
&email.VerifiedAt,
)
if err != nil {
@@ -63,130 +38,158 @@ func (u *userHuman) GetEmail(ctx context.Context, condition Condition) (*Email,
return &email, nil
}
func (h userHuman) Update(ctx context.Context, condition Condition, changes ...Change) error {
// Update implements [domain.HumanRepository].
func (h userHuman) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error {
h.builder.WriteString(`UPDATE human_users SET `)
Changes(changes).writeTo(&h.builder)
database.Changes(changes).Write(&h.builder)
h.writeCondition(condition)
stmt := h.builder.String()
return h.client.Exec(ctx, stmt, h.builder.args...)
return h.client.Exec(ctx, stmt, h.builder.Args()...)
}
func (h userHuman) SetFirstName(firstName string) Change {
return newChange(h.FirstNameColumn(), firstName)
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetFirstName implements [domain.humanChanges].
func (h userHuman) SetFirstName(firstName string) database.Change {
return database.NewChange(h.FirstNameColumn(), firstName)
}
func (h userHuman) FirstNameColumn() Column {
return column{"first_name"}
// SetLastName implements [domain.humanChanges].
func (h userHuman) SetLastName(lastName string) database.Change {
return database.NewChange(h.LastNameColumn(), lastName)
}
func (h userHuman) FirstNameCondition(op TextOperator, firstName string) Condition {
return newTextCondition(h.FirstNameColumn(), op, firstName)
}
func (h userHuman) SetLastName(lastName string) Change {
return newChange(h.LastNameColumn(), lastName)
}
func (h userHuman) LastNameColumn() Column {
return column{"last_name"}
}
func (h userHuman) LastNameCondition(op TextOperator, lastName string) Condition {
return newTextCondition(h.LastNameColumn(), op, lastName)
}
func (h userHuman) EmailAddressColumn() Column {
return ignoreCaseCol{
column: column{"email_address"},
suffix: "_lower",
}
}
func (h userHuman) EmailAddressCondition(op TextOperator, email string) Condition {
return newTextCondition(h.EmailAddressColumn(), op, email)
}
func (h userHuman) EmailVerifiedAtColumn() Column {
return column{"email_verified_at"}
}
func (h *userHuman) EmailAddressVerifiedCondition(isVerified bool) Condition {
if isVerified {
return IsNotNull(h.EmailVerifiedAtColumn())
}
return IsNull(h.EmailVerifiedAtColumn())
}
func (h userHuman) EmailVerifiedAtCondition(op TextOperator, emailVerifiedAt string) Condition {
return newTextCondition(h.EmailVerifiedAtColumn(), op, emailVerifiedAt)
}
func (h userHuman) SetEmailAddress(address string) Change {
return newChange(h.EmailAddressColumn(), address)
}
// SetEmailVerified sets the verified column of the email
// if at is zero the statement uses the database timestamp
func (h userHuman) SetEmailVerified(at time.Time) Change {
if at.IsZero() {
return newChange(h.EmailVerifiedAtColumn(), nowDBInstruction)
}
return newChange(h.EmailVerifiedAtColumn(), at)
}
func (h userHuman) SetEmail(address string, verified *time.Time) Change {
return newChanges(
// SetEmail implements [domain.humanChanges].
func (h userHuman) SetEmail(address string, verified *time.Time) database.Change {
return database.NewChanges(
h.SetEmailAddress(address),
newUpdatePtrColumn(h.EmailVerifiedAtColumn(), verified),
database.NewChangePtr(h.EmailVerifiedAtColumn(), verified),
)
}
func (h userHuman) PhoneNumberColumn() Column {
return column{"phone_number"}
// SetEmailAddress implements [domain.humanChanges].
func (h userHuman) SetEmailAddress(address string) database.Change {
return database.NewChange(h.EmailAddressColumn(), address)
}
func (h userHuman) SetPhoneNumber(number string) Change {
return newChange(h.PhoneNumberColumn(), number)
}
func (h userHuman) PhoneNumberCondition(op TextOperator, phoneNumber string) Condition {
return newTextCondition(h.PhoneNumberColumn(), op, phoneNumber)
}
func (h userHuman) PhoneVerifiedAtColumn() Column {
return column{"phone_verified_at"}
}
func (h userHuman) PhoneNumberVerifiedCondition(isVerified bool) Condition {
if isVerified {
return IsNotNull(h.PhoneVerifiedAtColumn())
}
return IsNull(h.PhoneVerifiedAtColumn())
}
// SetPhoneVerified sets the verified column of the phone
// if at is zero the statement uses the database timestamp
func (h userHuman) SetPhoneVerified(at time.Time) Change {
// SetEmailVerifiedAt implements [domain.humanChanges].
func (h userHuman) SetEmailVerifiedAt(at time.Time) database.Change {
if at.IsZero() {
return newChange(h.PhoneVerifiedAtColumn(), nowDBInstruction)
return database.NewChange(h.EmailVerifiedAtColumn(), database.NowInstruction)
}
return newChange(h.PhoneVerifiedAtColumn(), at)
return database.NewChange(h.EmailVerifiedAtColumn(), at)
}
func (h userHuman) PhoneVerifiedAtCondition(op TextOperator, phoneVerifiedAt string) Condition {
return newTextCondition(h.PhoneVerifiedAtColumn(), op, phoneVerifiedAt)
}
func (h userHuman) SetPhone(number string, verifiedAt *time.Time) Change {
return newChanges(
// SetPhone implements [domain.humanChanges].
func (h userHuman) SetPhone(number string, verifiedAt *time.Time) database.Change {
return database.NewChanges(
h.SetPhoneNumber(number),
newUpdatePtrColumn(h.PhoneVerifiedAtColumn(), verifiedAt),
database.NewChangePtr(h.PhoneVerifiedAtColumn(), verifiedAt),
)
}
func (h userHuman) columns() Columns {
// SetPhoneNumber implements [domain.humanChanges].
func (h userHuman) SetPhoneNumber(number string) database.Change {
return database.NewChange(h.PhoneNumberColumn(), number)
}
// SetPhoneVerifiedAt implements [domain.humanChanges].
func (h userHuman) SetPhoneVerifiedAt(at time.Time) database.Change {
if at.IsZero() {
return database.NewChange(h.PhoneVerifiedAtColumn(), database.NowInstruction)
}
return database.NewChange(h.PhoneVerifiedAtColumn(), at)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// FirstNameCondition implements [domain.humanConditions].
func (h userHuman) FirstNameCondition(op database.TextOperation, firstName string) database.Condition {
return database.NewTextCondition(h.FirstNameColumn(), op, firstName)
}
// LastNameCondition implements [domain.humanConditions].
func (h userHuman) LastNameCondition(op database.TextOperation, lastName string) database.Condition {
return database.NewTextCondition(h.LastNameColumn(), op, lastName)
}
// EmailAddressCondition implements [domain.humanConditions].
func (h userHuman) EmailAddressCondition(op database.TextOperation, email string) database.Condition {
return database.NewTextCondition(h.EmailAddressColumn(), op, email)
}
// EmailVerifiedCondition implements [domain.humanConditions].
func (h *userHuman) EmailVerifiedCondition(isVerified bool) database.Condition {
if isVerified {
return database.IsNotNull(h.EmailVerifiedAtColumn())
}
return database.IsNull(h.EmailVerifiedAtColumn())
}
// EmailVerifiedAtCondition implements [domain.humanConditions].
func (h userHuman) EmailVerifiedAtCondition(op database.NumberOperation, verifiedAt time.Time) database.Condition {
return database.NewNumberCondition(h.EmailVerifiedAtColumn(), op, verifiedAt)
}
// PhoneNumberCondition implements [domain.humanConditions].
func (h userHuman) PhoneNumberCondition(op database.TextOperation, phoneNumber string) database.Condition {
return database.NewTextCondition(h.PhoneNumberColumn(), op, phoneNumber)
}
// PhoneVerifiedCondition implements [domain.humanConditions].
func (h userHuman) PhoneVerifiedCondition(isVerified bool) database.Condition {
if isVerified {
return database.IsNotNull(h.PhoneVerifiedAtColumn())
}
return database.IsNull(h.PhoneVerifiedAtColumn())
}
// PhoneVerifiedAtCondition implements [domain.humanConditions].
func (h userHuman) PhoneVerifiedAtCondition(op database.NumberOperation, verifiedAt time.Time) database.Condition {
return database.NewNumberCondition(h.PhoneVerifiedAtColumn(), op, verifiedAt)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// FirstNameColumn implements [domain.humanColumns].
func (h userHuman) FirstNameColumn() database.Column {
return database.NewColumn("first_name")
}
// LastNameColumn implements [domain.humanColumns].
func (h userHuman) LastNameColumn() database.Column {
return database.NewColumn("last_name")
}
// EmailAddressColumn implements [domain.humanColumns].
func (h userHuman) EmailAddressColumn() database.Column {
return database.NewIgnoreCaseColumn("email_address", "_lower")
}
// EmailVerifiedAtColumn implements [domain.humanColumns].
func (h userHuman) EmailVerifiedAtColumn() database.Column {
return database.NewColumn("email_verified_at")
}
// PhoneNumberColumn implements [domain.humanColumns].
func (h userHuman) PhoneNumberColumn() database.Column {
return database.NewColumn("phone_number")
}
// PhoneVerifiedAtColumn implements [domain.humanColumns].
func (h userHuman) PhoneVerifiedAtColumn() database.Column {
return database.NewColumn("phone_verified_at")
}
func (h userHuman) columns() database.Columns {
return append(h.user.columns(),
h.FirstNameColumn(),
h.LastNameColumn(),
@@ -197,7 +200,7 @@ func (h userHuman) columns() Columns {
)
}
func (h userHuman) writeReturning(builder *statementBuilder) {
func (h userHuman) writeReturning(builder *database.StatementBuilder) {
builder.WriteString(" RETURNING ")
h.columns().writeTo(builder)
h.columns().Write(builder)
}

View File

@@ -1,72 +1,64 @@
package v4
import "context"
import (
"context"
type Machine struct {
Description string
}
func (Machine) userTrait() {}
func (m Machine) Type() UserType {
return UserTypeMachine
}
const UserTypeMachine UserType = "machine"
var _ userTrait = (*Machine)(nil)
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type userMachine struct {
*user
}
func (u *user) Machine() *userMachine {
return &userMachine{user: u}
}
var _ domain.MachineRepository = (*userMachine)(nil)
func (m userMachine) Update(ctx context.Context, condition Condition, changes ...Change) ([]*Machine, error) {
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
// Update implements [domain.MachineRepository].
func (m userMachine) Update(ctx context.Context, condition database.Condition, changes ...database.Change) (err error) {
m.builder.WriteString("UPDATE user_machines SET ")
Changes(changes).writeTo(&m.builder)
database.Changes(changes).Write(&m.builder)
m.writeCondition(condition)
m.writeReturning()
var machines []*Machine
rows, err := m.client.Query(ctx, m.builder.String(), m.builder.args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
machine := new(Machine)
if err := rows.Scan(&machine.Description); err != nil {
return nil, err
}
machines = append(machines, machine)
}
if err := rows.Err(); err != nil {
return nil, err
}
return machines, nil
return m.client.Exec(ctx, m.builder.String(), m.builder.Args()...)
}
func (userMachine) DescriptionColumn() Column {
return column{"description"}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetDescription implements [domain.machineChanges].
func (m userMachine) SetDescription(description string) database.Change {
return database.NewChange(m.DescriptionColumn(), description)
}
func (m userMachine) SetDescription(description string) Change {
return newChange(m.DescriptionColumn(), description)
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// DescriptionCondition implements [domain.machineConditions].
func (m userMachine) DescriptionCondition(op database.TextOperation, description string) database.Condition {
return database.NewTextCondition(m.DescriptionColumn(), op, description)
}
func (m userMachine) DescriptionCondition(op TextOperator, description string) Condition {
return newTextCondition(m.DescriptionColumn(), op, description)
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// DescriptionColumn implements [domain.machineColumns].
func (m userMachine) DescriptionColumn() database.Column {
return database.NewColumn("description")
}
func (m userMachine) columns() Columns {
func (m userMachine) columns() database.Columns {
return append(m.user.columns(), m.DescriptionColumn())
}
func (m *userMachine) writeReturning() {
m.builder.WriteString(" RETURNING ")
m.columns().writeTo(&m.builder)
m.columns().Write(&m.builder)
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/backend/v3/storage/database"
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
)
@@ -12,16 +13,16 @@ func TestQueryUser(t *testing.T) {
t.Run("User filters", func(t *testing.T) {
user := v4.UserRepository(nil)
u, err := user.Get(context.Background(),
v4.WithCondition(
v4.And(
v4.Or(
database.WithCondition(
database.And(
database.Or(
user.IDCondition("test"),
user.IDCondition("2"),
),
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"),
),
),
v4.WithOrderBy(user.CreatedAtColumn()),
database.WithOrderBy(user.CreatedAtColumn()),
)
assert.NoError(t, err)
@@ -32,12 +33,12 @@ func TestQueryUser(t *testing.T) {
user := v4.UserRepository(nil)
machine := user.Machine()
human := user.Human()
email, err := human.GetEmail(context.Background(), v4.And(
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
v4.Or(
machine.DescriptionCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
human.EmailAddressVerifiedCondition(true),
v4.IsNotNull(machine.DescriptionColumn()),
email, err := human.GetEmail(context.Background(), database.And(
user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"),
database.Or(
machine.DescriptionCondition(database.TextOperationStartsWithIgnoreCase, "test"),
human.EmailVerifiedCondition(true),
database.IsNotNull(machine.DescriptionColumn()),
),
))
@@ -62,6 +63,6 @@ func TestArg(t *testing.T) {
func TestWriteUser(t *testing.T) {
t.Run("update user", func(t *testing.T) {
user := v4.UserRepository(nil)
user.Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
})
}

View File

@@ -31,17 +31,17 @@ var _ domain.UserOperation = (*userOperation)(nil)
func UserIDQuery(id string) domain.UserClause {
return textClause[string]{
clause: clause[domain.TextOperation]{
clause: clause[database.TextOperation]{
field: userFields[domain.UserFieldID],
op: domain.TextOperationEqual,
op: database.TextOperationEqual,
},
value: id,
}
}
func HumanEmailQuery(op domain.TextOperation, email string) domain.UserClause {
func HumanEmailQuery(op database.TextOperation, email string) domain.UserClause {
return textClause[string]{
clause: clause[domain.TextOperation]{
clause: clause[database.TextOperation]{
field: userFields[domain.UserHumanFieldEmail],
op: op,
},
@@ -49,9 +49,9 @@ func HumanEmailQuery(op domain.TextOperation, email string) domain.UserClause {
}
}
func HumanEmailVerifiedQuery(op domain.BoolOperation) domain.UserClause {
return boolClause[domain.BoolOperation]{
clause: clause[domain.BoolOperation]{
func HumanEmailVerifiedQuery(op database.BoolOperation) domain.UserClause {
return boolClause[database.BoolOperation]{
clause: clause[database.BoolOperation]{
field: userFields[domain.UserHumanFieldEmailVerified],
op: op,
},

View File

@@ -0,0 +1,50 @@
package database
import (
"strconv"
"strings"
)
type Instruction string
const (
NowInstruction Instruction = "NOW()"
NullInstruction Instruction = "NULL"
)
type StatementBuilder struct {
strings.Builder
args []any
existingArgs map[any]string
}
func (b *StatementBuilder) WriteArg(arg any) {
b.WriteString(b.AppendArg(arg))
}
func (b *StatementBuilder) AppendArg(arg any) (placeholder string) {
if b.existingArgs == nil {
b.existingArgs = make(map[any]string)
}
if placeholder, ok := b.existingArgs[arg]; ok {
return placeholder
}
if instruction, ok := arg.(Instruction); ok {
return string(instruction)
}
b.args = append(b.args, arg)
placeholder = "$" + strconv.Itoa(len(b.args))
b.existingArgs[arg] = placeholder
return placeholder
}
func (b *StatementBuilder) AppendArgs(args ...any) {
for _, arg := range args {
b.AppendArg(arg)
}
}
func (b *StatementBuilder) Args() []any {
return b.args
}