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

View File

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

View File

@@ -14,7 +14,7 @@ func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedComma
return &EmailVerifiedCommand{ return &EmailVerifiedCommand{
UserID: userID, UserID: userID,
Email: &Email{ 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" "context"
"time" "time"
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4" "github.com/zitadel/zitadel/backend/v3/storage/database"
) )
type userColumns interface { type userColumns interface {
// TODO: move v4.columns to domain // InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() v4.Column InstanceIDColumn() database.Column
OrgIDColumn() v4.Column // OrgIDColumn returns the column for the org id field.
IDColumn() v4.Column OrgIDColumn() database.Column
usernameColumn() v4.Column // IDColumn returns the column for the id field.
CreatedAtColumn() v4.Column IDColumn() database.Column
UpdatedAtColumn() v4.Column // UsernameColumn returns the column for the username field.
DeletedAtColumn() v4.Column 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 { type userConditions interface {
InstanceIDCondition(instanceID string) v4.Condition // InstanceIDCondition returns an equal filter on the instance id field.
OrgIDCondition(orgID string) v4.Condition InstanceIDCondition(instanceID string) database.Condition
IDCondition(userID string) v4.Condition // OrgIDCondition returns an equal filter on the org id field.
UsernameCondition(op v4.TextOperator, username string) v4.Condition OrgIDCondition(orgID string) database.Condition
CreatedAtCondition(op v4.NumberOperator, createdAt time.Time) v4.Condition // IDCondition returns an equal filter on the id field.
UpdatedAtCondition(op v4.NumberOperator, updatedAt time.Time) v4.Condition IDCondition(userID string) database.Condition
DeletedCondition(isDeleted bool) v4.Condition // UsernameCondition returns a filter on the username field.
DeletedAtCondition(op v4.NumberOperator, deletedAt time.Time) v4.Condition 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 { type userChanges interface {
SetUsername(username string) v4.Change // SetUsername sets the username column.
SetUsername(username string) database.Change
} }
type UserRepository interface { type UserRepository interface {
userColumns userColumns
userConditions userConditions
userChanges userChanges
// TODO: move condition to domain // Get returns a user based on the given condition.
Get(ctx context.Context, opts v4.QueryOption) (*User, error) Get(ctx context.Context, opts ...database.QueryOption) (*User, error)
List(ctx context.Context, opts v4.QueryOption) ([]*User, error) // List returns a list of users based on the given condition.
Delete(ctx context.Context, condition v4.Condition) error 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 Human() HumanRepository
// Machine returns the [MachineRepository].
Machine() MachineRepository Machine() MachineRepository
} }
type humanColumns interface { type humanColumns interface {
userColumns userColumns
FirstNameColumn() v4.Column // FirstNameColumn returns the column for the first name field.
LastNameColumn() v4.Column FirstNameColumn() database.Column
EmailAddressColumn() v4.Column // LastNameColumn returns the column for the last name field.
EmailVerifiedAtColumn() v4.Column LastNameColumn() database.Column
PhoneNumberColumn() v4.Column // EmailAddressColumn returns the column for the email address field.
PhoneVerifiedAtColumn() v4.Column 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 { type humanConditions interface {
userConditions userConditions
FirstNameCondition(op v4.TextOperator, firstName string) v4.Condition // FirstNameCondition returns a filter on the first name field.
LastNameCondition(op v4.TextOperator, lastName string) v4.Condition FirstNameCondition(op database.TextOperation, firstName string) database.Condition
EmailAddressCondition(op v4.TextOperator, email string) v4.Condition // LastNameCondition returns a filter on the last name field.
EmailAddressVerifiedCondition(isVerified bool) v4.Condition LastNameCondition(op database.TextOperation, lastName string) database.Condition
EmailVerifiedAtCondition(op v4.TextOperator, emailVerifiedAt string) v4.Condition // EmailAddressCondition returns a filter on the email address field.
PhoneNumberCondition(op v4.TextOperator, phoneNumber string) v4.Condition EmailAddressCondition(op database.TextOperation, email string) database.Condition
PhoneNumberVerifiedCondition(isVerified bool) v4.Condition // EmailVerifiedCondition returns a filter that checks if the email is verified or not.
PhoneVerifiedAtCondition(op v4.TextOperator, phoneVerifiedAt string) v4.Condition 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 { type humanChanges interface {
userChanges userChanges
SetFirstName(firstName string) v4.Change // SetFirstName sets the first name field of the human.
SetLastName(lastName string) v4.Change 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 // SetEmail sets the email address and verified field of the email
SetEmailAddress(email string) v4.Change // if verifiedAt is nil the email is not verified
SetEmailVerifiedAt(emailVerifiedAt time.Time) v4.Change 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 // SetPhone sets the phone number and verified field
SetPhoneNumber(phoneNumber string) v4.Change // if verifiedAt is nil the phone is not verified
SetPhoneVerifiedAt(phoneVerifiedAt time.Time) v4.Change 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 { type HumanRepository interface {
@@ -87,144 +134,95 @@ type HumanRepository interface {
humanConditions humanConditions
humanChanges humanChanges
GetEmail(ctx context.Context, condition v4.Condition) (*Email, error) // Get returns an email based on the given condition.
// TODO: replace any with add email update columns GetEmail(ctx context.Context, condition database.Condition) (*Email, error)
Create(ctx context.Context, user *User) error // Update updates human users based on the given condition and changes.
Update(ctx context.Context, condition v4.Condition, changes ...v4.Change) error Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
} }
type machineColumns interface { type machineColumns interface {
userColumns userColumns
DescriptionColumn() v4.Column // DescriptionColumn returns the column for the description field.
DescriptionColumn() database.Column
} }
type machineConditions interface { type machineConditions interface {
userConditions 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 { type machineChanges interface {
userChanges userChanges
SetDescription(description string) v4.Change // SetDescription sets the description field of the machine.
SetDescription(description string) database.Change
} }
type MachineRepository interface { 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 machineColumns
machineConditions machineConditions
machineChanges machineChanges
Create(ctx context.Context, user *User) error
Update(ctx context.Context, condition v4.Condition, changes ...v4.Change) error
} }
// type UserRepository interface { type UserTraits interface {
// // Get(ctx context.Context, clauses ...UserClause) (*User, error) Type() UserType
// // Search(ctx context.Context, clauses ...UserClause) ([]*User, error) }
// UserQuery[UserOperation] type UserType string
// Human() HumanQuery
// Machine() MachineQuery
// }
// type UserQuery[Op UserOperation] interface { const (
// ByID(id string) UserQuery[Op] UserTypeHuman UserType = "human"
// Username(username string) UserQuery[Op] UserTypeMachine UserType = "machine"
// 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
// }
type User struct { 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 { type Email struct {
v4.Email Address string `json:"address"`
IsVerified bool VerifiedAt time.Time `json:"verifiedAt"`
} }
// type userTraits interface { type Phone struct {
// isUserTraits() Number string `json:"number"`
// } VerifiedAt time.Time `json:"verifiedAt"`
}
// type Human struct { type Machine struct {
// Email *Email `json:"email"` Description string `json:"description"`
// } }
// func (*Human) isUserTraits() {} // Type implements [UserTraits].
func (m *Machine) Type() UserType {
return UserTypeMachine
}
// type Machine struct { var _ UserTraits = (*Machine)(nil)
// Description string `json:"description"`
// }
// func (*Machine) isUserTraits() {}
// type Email struct {
// Address string `json:"address"`
// IsVerified bool `json:"isVerified"`
// }

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 { type Condition interface {
writeTo(builder *statementBuilder) Write(builder *StatementBuilder)
} }
type and struct { type and struct {
conditions []Condition conditions []Condition
} }
// writeTo implements [Condition]. // Write implements [Condition].
func (a *and) writeTo(builder *statementBuilder) { func (a *and) Write(builder *StatementBuilder) {
if len(a.conditions) > 1 { if len(a.conditions) > 1 {
builder.WriteString("(") builder.WriteString("(")
defer builder.WriteString(")") defer builder.WriteString(")")
@@ -18,7 +18,7 @@ func (a *and) writeTo(builder *statementBuilder) {
if i > 0 { if i > 0 {
builder.WriteString(" AND ") builder.WriteString(" AND ")
} }
condition.writeTo(builder) condition.(Condition).Write(builder)
} }
} }
@@ -32,8 +32,8 @@ type or struct {
conditions []Condition conditions []Condition
} }
// writeTo implements [Condition]. // Write implements [Condition].
func (o *or) writeTo(builder *statementBuilder) { func (o *or) Write(builder *StatementBuilder) {
if len(o.conditions) > 1 { if len(o.conditions) > 1 {
builder.WriteString("(") builder.WriteString("(")
defer builder.WriteString(")") defer builder.WriteString(")")
@@ -42,7 +42,7 @@ func (o *or) writeTo(builder *statementBuilder) {
if i > 0 { if i > 0 {
builder.WriteString(" OR ") builder.WriteString(" OR ")
} }
condition.writeTo(builder) condition.(Condition).Write(builder)
} }
} }
@@ -56,9 +56,9 @@ type isNull struct {
column Column column Column
} }
// writeTo implements [Condition]. // Write implements [Condition].
func (i *isNull) writeTo(builder *statementBuilder) { func (i *isNull) Write(builder *StatementBuilder) {
i.column.writeTo(builder) i.column.Write(builder)
builder.WriteString(" IS NULL") builder.WriteString(" IS NULL")
} }
@@ -72,40 +72,40 @@ type isNotNull struct {
column Column column Column
} }
// writeTo implements [Condition]. // Write implements [Condition].
func (i *isNotNull) writeTo(builder *statementBuilder) { func (i *isNotNull) Write(builder *StatementBuilder) {
i.column.writeTo(builder) i.column.Write(builder)
builder.WriteString(" IS NOT NULL") builder.WriteString(" IS NOT NULL")
} }
func IsNotNull(column Column) *isNotNull { func IsNotNull(column Column) *isNotNull {
return &isNotNull{column: column} return &isNotNull{column: column.(Column)}
} }
var _ Condition = (*isNotNull)(nil) 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 { func NewTextCondition[V Text](col Column, op TextOperation, value V) Condition {
return valueCondition(func(builder *statementBuilder) { return valueCondition(func(builder *StatementBuilder) {
writeTextOperation(builder, col, op, value) writeTextOperation(builder, col, op, value)
}) })
} }
func newNumberCondition[V Number](col Column, op NumberOperator, value V) Condition { func NewNumberCondition[V Number](col Column, op NumberOperation, value V) Condition {
return valueCondition(func(builder *statementBuilder) { return valueCondition(func(builder *StatementBuilder) {
writeNumberOperation(builder, col, op, value) writeNumberOperation(builder, col, op, value)
}) })
} }
func newBooleanCondition[V Boolean](col Column, value V) Condition { func NewBooleanCondition[V Boolean](col Column, value V) Condition {
return valueCondition(func(builder *statementBuilder) { return valueCondition(func(builder *StatementBuilder) {
writeBooleanOperation(builder, col, value) writeBooleanOperation(builder, col, value)
}) })
} }
// writeTo implements [Condition]. // Write implements [Condition].
func (c valueCondition) writeTo(builder *statementBuilder) { func (c valueCondition) Write(builder *StatementBuilder) {
c(builder) 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 InstanceID string
ID string ID string
Name string Name string
Dates
} }
type GetOrg struct{} 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" "context"
"time" "time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database" "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,` + 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` + ` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
` FROM users_view` ` FROM users_view`
type user struct { type user struct {
builder statementBuilder builder database.StatementBuilder
client database.QueryExecutor client database.QueryExecutor
} }
func UserRepository(client database.QueryExecutor) *user { func UserRepository(client database.QueryExecutor) domain.UserRepository {
return &user{ return &user{
client: client, client: client,
} }
} }
func (u *user) List(ctx context.Context, opts ...QueryOption) (users []*User, err error) { var _ domain.UserRepository = (*user)(nil)
options := new(queryOpts)
// -------------------------------------------------------------
// 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 { for _, opt := range opts {
opt(options) opt(options)
} }
u.builder.WriteString(queryUserStmt) u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder) options.WriteCondition(&u.builder)
options.writeOrderBy(&u.builder) options.WriteOrderBy(&u.builder)
options.writeLimit(&u.builder) options.WriteLimit(&u.builder)
options.writeOffset(&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 { if err != nil {
return nil, err return nil, err
} }
@@ -80,142 +76,157 @@ func (u *user) List(ctx context.Context, opts ...QueryOption) (users []*User, er
return users, nil return users, nil
} }
func (u *user) Get(ctx context.Context, opts ...QueryOption) (*User, error) { // Get implements [domain.UserRepository].
options := new(queryOpts) func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) {
options := new(database.QueryOpts)
for _, opt := range opts { for _, opt := range opts {
opt(options) opt(options)
} }
u.builder.WriteString(queryUserStmt) u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder) options.WriteCondition(&u.builder)
options.writeOrderBy(&u.builder) options.WriteOrderBy(&u.builder)
options.writeLimit(&u.builder) options.WriteLimit(&u.builder)
options.writeOffset(&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 ( const (
// TODO: change to separate statements and tables 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)` +
createUserCte = `WITH user AS (` + ` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` +
`INSERT INTO users (instance_id, org_id, id, username, type) VALUES ($1, $2, $3, $4, $5)` + ` RETURNING created_at, updated_at`
` RETURNING *)` createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` +
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)` + ` VALUES ($1, $2, $3, $4, $5)` +
` SELECT u.instance_id, u.org_id, u.id, $6, $7, $8, $9, $10, $11` + ` RETURNING created_at, updated_at`
` 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`
) )
func (u *user) Create(ctx context.Context, user *User) error { // Create implements [domain.UserRepository].
u.builder.appendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type()) 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) { switch trait := user.Traits.(type) {
case *Human: case *domain.Human:
u.builder.WriteString(createHumanStmt) u.builder.WriteString(createHumanStmt)
u.builder.appendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt) u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
case *Machine: case *domain.Machine:
u.builder.WriteString(createMachineStmt) 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 { // Delete implements [domain.UserRepository].
u.builder.WriteString("UPDATE users SET ") func (u *user) Delete(ctx context.Context, condition database.Condition) error {
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 {
u.builder.WriteString("DELETE FROM users") u.builder.WriteString("DELETE FROM users")
u.writeCondition(condition) 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 { // OrgIDCondition implements [domain.userConditions].
return column{name: "org_id"} func (u user) OrgIDCondition(orgID string) database.Condition {
return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID)
} }
func (u *user) OrgIDCondition(orgID string) Condition { // IDCondition implements [domain.userConditions].
return newTextCondition(u.OrgIDColumn(), TextOperatorEqual, orgID) func (u user) IDCondition(userID string) database.Condition {
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
} }
func (u *user) IDColumn() Column { // UsernameCondition implements [domain.userConditions].
return column{name: "id"} 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 { // CreatedAtCondition implements [domain.userConditions].
return newTextCondition(u.IDColumn(), TextOperatorEqual, userID) func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt)
} }
func (u *user) UsernameColumn() Column { // UpdatedAtCondition implements [domain.userConditions].
return ignoreCaseCol{ func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
column: column{name: "username"}, return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
suffix: "_lower",
}
} }
func (u user) SetUsername(username string) Change { // DeletedCondition implements [domain.userConditions].
return newChange(u.UsernameColumn(), username) func (u user) DeletedCondition(isDeleted bool) database.Condition {
}
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 {
if isDeleted { 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 { // DeletedAtCondition implements [domain.userConditions].
return newNumberCondition(u.DeletedAtColumn(), op, deletedAt) 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 { if condition == nil {
return return
} }
u.builder.WriteString(" WHERE ") u.builder.WriteString(" WHERE ")
condition.writeTo(&u.builder) condition.Write(&u.builder)
} }
func (u user) columns() Columns { func (u user) columns() database.Columns {
return Columns{ return database.Columns{
u.InstanceIDColumn(), u.InstanceIDColumn(),
u.OrgIDColumn(), u.OrgIDColumn(),
u.IDColumn(), 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 ( var (
user User user domain.User
human Human human domain.Human
email Email email domain.Email
phone Phone phone domain.Phone
machine Machine machine domain.Machine
typ UserType typ domain.UserType
) )
err := scanner.Scan( err := scanner.Scan(
&user.InstanceID, &user.InstanceID,
@@ -241,9 +252,9 @@ func scanUser(scanner database.Scanner) (*User, error) {
&user.ID, &user.ID,
&user.Username, &user.Username,
&typ, &typ,
&user.Dates.CreatedAt, &user.CreatedAt,
&user.Dates.UpdatedAt, &user.UpdatedAt,
&user.Dates.DeletedAt, &user.DeletedAt,
&human.FirstName, &human.FirstName,
&human.LastName, &human.LastName,
&email.Address, &email.Address,
@@ -257,7 +268,7 @@ func scanUser(scanner database.Scanner) (*User, error) {
} }
switch typ { switch typ {
case UserTypeHuman: case domain.UserTypeHuman:
if email.Address != "" { if email.Address != "" {
human.Email = &email human.Email = &email
} }
@@ -265,7 +276,7 @@ func scanUser(scanner database.Scanner) (*User, error) {
human.Phone = &phone human.Phone = &phone
} }
user.Traits = &human user.Traits = &human
case UserTypeMachine: case domain.UserTypeMachine:
user.Traits = &machine user.Traits = &machine
} }

View File

@@ -3,58 +3,33 @@ package v4
import ( import (
"context" "context"
"time" "time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
) )
type Human struct { // -------------------------------------------------------------
FirstName string // repository
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
}
type userHuman struct { type userHuman struct {
*user *user
} }
func (u *user) Human() *userHuman { var _ domain.HumanRepository = (*userHuman)(nil)
return &userHuman{user: u}
}
const userEmailQuery = `SELECT h.email_address, h.email_verified_at FROM user_humans h` 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) { // GetEmail implements [domain.HumanRepository].
var email Email func (u *userHuman) GetEmail(ctx context.Context, condition database.Condition) (*domain.Email, error) {
var email domain.Email
u.builder.WriteString(userEmailQuery) u.builder.WriteString(userEmailQuery)
u.writeCondition(condition) 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.Address,
&email.Verification.VerifiedAt, &email.VerifiedAt,
) )
if err != nil { if err != nil {
@@ -63,130 +38,158 @@ func (u *userHuman) GetEmail(ctx context.Context, condition Condition) (*Email,
return &email, nil 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 `) h.builder.WriteString(`UPDATE human_users SET `)
Changes(changes).writeTo(&h.builder) database.Changes(changes).Write(&h.builder)
h.writeCondition(condition) h.writeCondition(condition)
stmt := h.builder.String() 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 { // SetLastName implements [domain.humanChanges].
return column{"first_name"} func (h userHuman) SetLastName(lastName string) database.Change {
return database.NewChange(h.LastNameColumn(), lastName)
} }
func (h userHuman) FirstNameCondition(op TextOperator, firstName string) Condition { // SetEmail implements [domain.humanChanges].
return newTextCondition(h.FirstNameColumn(), op, firstName) func (h userHuman) SetEmail(address string, verified *time.Time) database.Change {
} return database.NewChanges(
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(
h.SetEmailAddress(address), h.SetEmailAddress(address),
newUpdatePtrColumn(h.EmailVerifiedAtColumn(), verified), database.NewChangePtr(h.EmailVerifiedAtColumn(), verified),
) )
} }
func (h userHuman) PhoneNumberColumn() Column { // SetEmailAddress implements [domain.humanChanges].
return column{"phone_number"} func (h userHuman) SetEmailAddress(address string) database.Change {
return database.NewChange(h.EmailAddressColumn(), address)
} }
func (h userHuman) SetPhoneNumber(number string) Change { // SetEmailVerifiedAt implements [domain.humanChanges].
return newChange(h.PhoneNumberColumn(), number) func (h userHuman) SetEmailVerifiedAt(at time.Time) database.Change {
}
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 {
if at.IsZero() { 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 { // SetPhone implements [domain.humanChanges].
return newTextCondition(h.PhoneVerifiedAtColumn(), op, phoneVerifiedAt) func (h userHuman) SetPhone(number string, verifiedAt *time.Time) database.Change {
} return database.NewChanges(
func (h userHuman) SetPhone(number string, verifiedAt *time.Time) Change {
return newChanges(
h.SetPhoneNumber(number), 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(), return append(h.user.columns(),
h.FirstNameColumn(), h.FirstNameColumn(),
h.LastNameColumn(), 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 ") builder.WriteString(" RETURNING ")
h.columns().writeTo(builder) h.columns().Write(builder)
} }

View File

@@ -1,72 +1,64 @@
package v4 package v4
import "context" import (
"context"
type Machine struct { "github.com/zitadel/zitadel/backend/v3/domain"
Description string "github.com/zitadel/zitadel/backend/v3/storage/database"
} )
func (Machine) userTrait() {}
func (m Machine) Type() UserType {
return UserTypeMachine
}
const UserTypeMachine UserType = "machine"
var _ userTrait = (*Machine)(nil)
type userMachine struct { type userMachine struct {
*user *user
} }
func (u *user) Machine() *userMachine { var _ domain.MachineRepository = (*userMachine)(nil)
return &userMachine{user: u}
}
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 ") m.builder.WriteString("UPDATE user_machines SET ")
Changes(changes).writeTo(&m.builder) database.Changes(changes).Write(&m.builder)
m.writeCondition(condition) m.writeCondition(condition)
m.writeReturning() m.writeReturning()
var machines []*Machine return m.client.Exec(ctx, m.builder.String(), m.builder.Args()...)
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
} }
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()) return append(m.user.columns(), m.DescriptionColumn())
} }
func (m *userMachine) writeReturning() { func (m *userMachine) writeReturning() {
m.builder.WriteString(" RETURNING ") m.builder.WriteString(" RETURNING ")
m.columns().writeTo(&m.builder) m.columns().Write(&m.builder)
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "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" 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) { t.Run("User filters", func(t *testing.T) {
user := v4.UserRepository(nil) user := v4.UserRepository(nil)
u, err := user.Get(context.Background(), u, err := user.Get(context.Background(),
v4.WithCondition( database.WithCondition(
v4.And( database.And(
v4.Or( database.Or(
user.IDCondition("test"), user.IDCondition("test"),
user.IDCondition("2"), 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) assert.NoError(t, err)
@@ -32,12 +33,12 @@ func TestQueryUser(t *testing.T) {
user := v4.UserRepository(nil) user := v4.UserRepository(nil)
machine := user.Machine() machine := user.Machine()
human := user.Human() human := user.Human()
email, err := human.GetEmail(context.Background(), v4.And( email, err := human.GetEmail(context.Background(), database.And(
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"), user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"),
v4.Or( database.Or(
machine.DescriptionCondition(v4.TextOperatorStartsWithIgnoreCase, "test"), machine.DescriptionCondition(database.TextOperationStartsWithIgnoreCase, "test"),
human.EmailAddressVerifiedCondition(true), human.EmailVerifiedCondition(true),
v4.IsNotNull(machine.DescriptionColumn()), database.IsNotNull(machine.DescriptionColumn()),
), ),
)) ))
@@ -62,6 +63,6 @@ func TestArg(t *testing.T) {
func TestWriteUser(t *testing.T) { func TestWriteUser(t *testing.T) {
t.Run("update user", func(t *testing.T) { t.Run("update user", func(t *testing.T) {
user := v4.UserRepository(nil) 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 { func UserIDQuery(id string) domain.UserClause {
return textClause[string]{ return textClause[string]{
clause: clause[domain.TextOperation]{ clause: clause[database.TextOperation]{
field: userFields[domain.UserFieldID], field: userFields[domain.UserFieldID],
op: domain.TextOperationEqual, op: database.TextOperationEqual,
}, },
value: id, value: id,
} }
} }
func HumanEmailQuery(op domain.TextOperation, email string) domain.UserClause { func HumanEmailQuery(op database.TextOperation, email string) domain.UserClause {
return textClause[string]{ return textClause[string]{
clause: clause[domain.TextOperation]{ clause: clause[database.TextOperation]{
field: userFields[domain.UserHumanFieldEmail], field: userFields[domain.UserHumanFieldEmail],
op: op, op: op,
}, },
@@ -49,9 +49,9 @@ func HumanEmailQuery(op domain.TextOperation, email string) domain.UserClause {
} }
} }
func HumanEmailVerifiedQuery(op domain.BoolOperation) domain.UserClause { func HumanEmailVerifiedQuery(op database.BoolOperation) domain.UserClause {
return boolClause[domain.BoolOperation]{ return boolClause[database.BoolOperation]{
clause: clause[domain.BoolOperation]{ clause: clause[database.BoolOperation]{
field: userFields[domain.UserHumanFieldEmailVerified], field: userFields[domain.UserHumanFieldEmailVerified],
op: op, 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
}