mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-23 14:37:49 +00:00
This pull request introduces a significant refactoring of the database interaction layer, focusing on improving explicitness, transactional control, and error handling. The core change is the removal of the stateful `QueryExecutor` from repository instances. Instead, it is now passed as an argument to each method that interacts with the database. This change makes transaction management more explicit and flexible, as the same repository instance can be used with a database pool or a specific transaction without needing to be re-instantiated. ### Key Changes - **Explicit `QueryExecutor` Passing:** - All repository methods (`Get`, `List`, `Create`, `Update`, `Delete`, etc.) in `InstanceRepository`, `OrganizationRepository`, `UserRepository`, and their sub-repositories now require a `database.QueryExecutor` (e.g., a `*pgxpool.Pool` or `pgx.Tx`) as the first argument. - Repository constructors no longer accept a `QueryExecutor`. For example, `repository.InstanceRepository(pool)` is now `repository.InstanceRepository()`. - **Enhanced Error Handling:** - A new `database.MissingConditionError` is introduced to enforce required query conditions, such as ensuring an `instance_id` is always present in `UPDATE` and `DELETE` operations. - The database error wrapper in the `postgres` package now correctly identifies and wraps `pgx.ErrTooManyRows` and similar errors from the `scany` library into a `database.MultipleRowsFoundError`. - **Improved Database Conditions:** - The `database.Condition` interface now includes a `ContainsColumn(Column) bool` method. This allows for runtime checks to ensure that critical filters (like `instance_id`) are included in a query, preventing accidental cross-tenant data modification. - A new `database.Exists()` condition has been added to support `EXISTS` subqueries, enabling more complex filtering logic, such as finding an organization that has a specific domain. - **Repository and Interface Refactoring:** - The method for loading related entities (e.g., domains for an organization) has been changed from a boolean flag (`Domains(true)`) to a more explicit, chainable method (`LoadDomains()`). This returns a new repository instance configured to load the sub-resource, promoting immutability. - The custom `OrgIdentifierCondition` has been removed in favor of using the standard `database.Condition` interface, simplifying the API. - **Code Cleanup and Test Updates:** - Unnecessary struct embeddings and metadata have been removed. - All integration and repository tests have been updated to reflect the new method signatures, passing the database pool or transaction object explicitly. - New tests have been added to cover the new `ExistsDomain` functionality and other enhancements. These changes make the data access layer more robust, predictable, and easier to work with, especially in the context of database transactions.
242 lines
9.2 KiB
Go
242 lines
9.2 KiB
Go
package domain
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
)
|
|
|
|
// userColumns define all the columns of the user table.
|
|
type userColumns interface {
|
|
// 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
|
|
}
|
|
|
|
// userConditions define all the conditions for the user table.
|
|
type userConditions interface {
|
|
// 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
|
|
}
|
|
|
|
// userChanges define all the changes for the user table.
|
|
type userChanges interface {
|
|
// SetUsername sets the username column.
|
|
SetUsername(username string) database.Change
|
|
}
|
|
|
|
// UserRepository is the interface for the user repository.
|
|
type UserRepository interface {
|
|
userColumns
|
|
userConditions
|
|
userChanges
|
|
// Get returns a user based on the given condition.
|
|
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*User, error)
|
|
// List returns a list of users based on the given condition.
|
|
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*User, error)
|
|
// Create creates a new user.
|
|
Create(ctx context.Context, client database.QueryExecutor, user *User) error
|
|
// Delete removes users based on the given condition.
|
|
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) error
|
|
// Human returns the [HumanRepository].
|
|
Human() HumanRepository
|
|
// Machine returns the [MachineRepository].
|
|
Machine() MachineRepository
|
|
}
|
|
|
|
// humanColumns define all the columns of the human table which inherits the user table.
|
|
type humanColumns interface {
|
|
userColumns
|
|
// 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
|
|
}
|
|
|
|
// humanConditions define all the conditions for the human table which inherits the user table.
|
|
type humanConditions interface {
|
|
userConditions
|
|
// 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
|
|
}
|
|
|
|
// humanChanges define all the changes for the human table which inherits the user table.
|
|
type humanChanges interface {
|
|
userChanges
|
|
// 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 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 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
|
|
}
|
|
|
|
// HumanRepository is the interface for the human repository it inherits the user repository.
|
|
type HumanRepository interface {
|
|
humanColumns
|
|
humanConditions
|
|
humanChanges
|
|
|
|
// Get returns an email based on the given condition.
|
|
GetEmail(ctx context.Context, client database.QueryExecutor, condition database.Condition) (*Email, error)
|
|
// Update updates human users based on the given condition and changes.
|
|
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) error
|
|
}
|
|
|
|
// machineColumns define all the columns of the machine table which inherits the user table.
|
|
type machineColumns interface {
|
|
userColumns
|
|
// DescriptionColumn returns the column for the description field.
|
|
DescriptionColumn() database.Column
|
|
}
|
|
|
|
// machineConditions define all the conditions for the machine table which inherits the user table.
|
|
type machineConditions interface {
|
|
userConditions
|
|
// DescriptionCondition returns a filter on the description field.
|
|
DescriptionCondition(op database.TextOperation, description string) database.Condition
|
|
}
|
|
|
|
// machineChanges define all the changes for the machine table which inherits the user table.
|
|
type machineChanges interface {
|
|
userChanges
|
|
// SetDescription sets the description field of the machine.
|
|
SetDescription(description string) database.Change
|
|
}
|
|
|
|
// MachineRepository is the interface for the machine repository it inherits the user repository.
|
|
type MachineRepository interface {
|
|
// Update updates machine users based on the given condition and changes.
|
|
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) error
|
|
|
|
machineColumns
|
|
machineConditions
|
|
machineChanges
|
|
}
|
|
|
|
// UserTraits is implemented by [Human] and [Machine].
|
|
type UserTraits interface {
|
|
Type() UserType
|
|
}
|
|
|
|
type UserType string
|
|
|
|
const (
|
|
UserTypeHuman UserType = "human"
|
|
UserTypeMachine UserType = "machine"
|
|
)
|
|
|
|
type User struct {
|
|
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 {
|
|
Address string `json:"address"`
|
|
VerifiedAt time.Time `json:"verifiedAt"`
|
|
}
|
|
|
|
type Phone struct {
|
|
Number string `json:"number"`
|
|
VerifiedAt time.Time `json:"verifiedAt"`
|
|
}
|
|
|
|
type Machine struct {
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// Type implements [UserTraits].
|
|
func (m *Machine) Type() UserType {
|
|
return UserTypeMachine
|
|
}
|
|
|
|
var _ UserTraits = (*Machine)(nil)
|