From 6ba86bc67b031e4c79574d2f4b9c7fc6d5d7eb0d Mon Sep 17 00:00:00 2001 From: adlerhurst <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 6 May 2025 07:18:11 +0200 Subject: [PATCH] move files --- backend/v3/domain/create_user.go | 7 +- backend/v3/domain/domain_test.go | 70 ++-- backend/v3/domain/email_verification.go | 2 +- backend/v3/domain/repository.go | 82 ----- backend/v3/domain/user.go | 306 +++++++++--------- backend/v3/storage/database/change.go | 51 +++ backend/v3/storage/database/column.go | 55 ++++ .../{repository/stmt/v4 => }/condition.go | 48 +-- backend/v3/storage/database/operators.go | 139 ++++++++ backend/v3/storage/database/query.go | 66 ++++ .../database/repository/stmt/v4/column.go | 89 ----- .../database/repository/stmt/v4/operators.go | 139 -------- .../database/repository/stmt/v4/org.go | 1 - .../database/repository/stmt/v4/query.go | 66 ---- .../database/repository/stmt/v4/statement.go | 46 --- .../database/repository/stmt/v4/user.go | 275 ++++++++-------- .../database/repository/stmt/v4/user_human.go | 279 ++++++++-------- .../repository/stmt/v4/user_machine.go | 82 +++-- .../database/repository/stmt/v4/user_test.go | 25 +- .../database/repository/user_operation.go | 14 +- backend/v3/storage/database/statement.go | 50 +++ 21 files changed, 916 insertions(+), 976 deletions(-) delete mode 100644 backend/v3/domain/repository.go create mode 100644 backend/v3/storage/database/change.go create mode 100644 backend/v3/storage/database/column.go rename backend/v3/storage/database/{repository/stmt/v4 => }/condition.go (53%) create mode 100644 backend/v3/storage/database/operators.go create mode 100644 backend/v3/storage/database/query.go delete mode 100644 backend/v3/storage/database/repository/stmt/v4/column.go delete mode 100644 backend/v3/storage/database/repository/stmt/v4/operators.go delete mode 100644 backend/v3/storage/database/repository/stmt/v4/query.go delete mode 100644 backend/v3/storage/database/repository/stmt/v4/statement.go create mode 100644 backend/v3/storage/database/statement.go diff --git a/backend/v3/domain/create_user.go b/backend/v3/domain/create_user.go index c4f480d1cb..dc19dbbbbd 100644 --- a/backend/v3/domain/create_user.go +++ b/backend/v3/domain/create_user.go @@ -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{}, - }, + Username: username, + Traits: &Human{}, }, } diff --git a/backend/v3/domain/domain_test.go b/backend/v3/domain/domain_test.go index 6e4ee248f6..f6ca8e3615 100644 --- a/backend/v3/domain/domain_test.go +++ b/backend/v3/domain/domain_test.go @@ -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) +// }) +// } diff --git a/backend/v3/domain/email_verification.go b/backend/v3/domain/email_verification.go index 9d9be0990f..062d742cae 100644 --- a/backend/v3/domain/email_verification.go +++ b/backend/v3/domain/email_verification.go @@ -14,7 +14,7 @@ func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedComma return &EmailVerifiedCommand{ UserID: userID, Email: &Email{ - IsVerified: isVerified, + VerifiedAt: time.Time{}, }, } } diff --git a/backend/v3/domain/repository.go b/backend/v3/domain/repository.go deleted file mode 100644 index f9ea8b92f4..0000000000 --- a/backend/v3/domain/repository.go +++ /dev/null @@ -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() {} diff --git a/backend/v3/domain/user.go b/backend/v3/domain/user.go index 5c07313eb6..99d86d2467 100644 --- a/backend/v3/domain/user.go +++ b/backend/v3/domain/user.go @@ -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) diff --git a/backend/v3/storage/database/change.go b/backend/v3/storage/database/change.go new file mode 100644 index 0000000000..b5e6dfbba3 --- /dev/null +++ b/backend/v3/storage/database/change.go @@ -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) diff --git a/backend/v3/storage/database/column.go b/backend/v3/storage/database/column.go new file mode 100644 index 0000000000..801926730d --- /dev/null +++ b/backend/v3/storage/database/column.go @@ -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) +} diff --git a/backend/v3/storage/database/repository/stmt/v4/condition.go b/backend/v3/storage/database/condition.go similarity index 53% rename from backend/v3/storage/database/repository/stmt/v4/condition.go rename to backend/v3/storage/database/condition.go index e9cfad4317..a58952660f 100644 --- a/backend/v3/storage/database/repository/stmt/v4/condition.go +++ b/backend/v3/storage/database/condition.go @@ -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) } diff --git a/backend/v3/storage/database/operators.go b/backend/v3/storage/database/operators.go new file mode 100644 index 0000000000..d8e34d8b8b --- /dev/null +++ b/backend/v3/storage/database/operators.go @@ -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)) +} diff --git a/backend/v3/storage/database/query.go b/backend/v3/storage/database/query.go new file mode 100644 index 0000000000..73d4d1ac87 --- /dev/null +++ b/backend/v3/storage/database/query.go @@ -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) +} diff --git a/backend/v3/storage/database/repository/stmt/v4/column.go b/backend/v3/storage/database/repository/stmt/v4/column.go deleted file mode 100644 index c19834db13..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/column.go +++ /dev/null @@ -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) -} diff --git a/backend/v3/storage/database/repository/stmt/v4/operators.go b/backend/v3/storage/database/repository/stmt/v4/operators.go deleted file mode 100644 index 44f7568184..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/operators.go +++ /dev/null @@ -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)) -} diff --git a/backend/v3/storage/database/repository/stmt/v4/org.go b/backend/v3/storage/database/repository/stmt/v4/org.go index 42681d2442..fd39bb31f8 100644 --- a/backend/v3/storage/database/repository/stmt/v4/org.go +++ b/backend/v3/storage/database/repository/stmt/v4/org.go @@ -4,7 +4,6 @@ type Org struct { InstanceID string ID string Name string - Dates } type GetOrg struct{} diff --git a/backend/v3/storage/database/repository/stmt/v4/query.go b/backend/v3/storage/database/repository/stmt/v4/query.go deleted file mode 100644 index 01f22e2c25..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/query.go +++ /dev/null @@ -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 - } -} diff --git a/backend/v3/storage/database/repository/stmt/v4/statement.go b/backend/v3/storage/database/repository/stmt/v4/statement.go deleted file mode 100644 index 9b652ee805..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/statement.go +++ /dev/null @@ -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) - } -} diff --git a/backend/v3/storage/database/repository/stmt/v4/user.go b/backend/v3/storage/database/repository/stmt/v4/user.go index 4c620c43c6..8613611e29 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user.go +++ b/backend/v3/storage/database/repository/stmt/v4/user.go @@ -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 } diff --git a/backend/v3/storage/database/repository/stmt/v4/user_human.go b/backend/v3/storage/database/repository/stmt/v4/user_human.go index ab13eab18d..05fa794f8f 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_human.go +++ b/backend/v3/storage/database/repository/stmt/v4/user_human.go @@ -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) } diff --git a/backend/v3/storage/database/repository/stmt/v4/user_machine.go b/backend/v3/storage/database/repository/stmt/v4/user_machine.go index c86f9493af..501722bd8a 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_machine.go +++ b/backend/v3/storage/database/repository/stmt/v4/user_machine.go @@ -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) } diff --git a/backend/v3/storage/database/repository/stmt/v4/user_test.go b/backend/v3/storage/database/repository/stmt/v4/user_test.go index 431599bccb..5b81fcc259 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_test.go +++ b/backend/v3/storage/database/repository/stmt/v4/user_test.go @@ -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")) }) } diff --git a/backend/v3/storage/database/repository/user_operation.go b/backend/v3/storage/database/repository/user_operation.go index f2e90dc55b..eea24463b8 100644 --- a/backend/v3/storage/database/repository/user_operation.go +++ b/backend/v3/storage/database/repository/user_operation.go @@ -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, }, diff --git a/backend/v3/storage/database/statement.go b/backend/v3/storage/database/statement.go new file mode 100644 index 0000000000..55e874a5a7 --- /dev/null +++ b/backend/v3/storage/database/statement.go @@ -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 +}