mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-31 12:16:49 +00:00
refactor(rt): add primary key columns to repo interfaces (#10843)
# Which Problems Are Solved Relational table's primary keys are part of the SQL table definition. However, by abstraction through the repository interfaces callers (service layer) have no clear understanding of the primary key definion. This might lead to confusion during implementation: - The repository makes a sanity check on required conditions before query execution and returns an error if the condition was not met. - When a Get method is executed, an error is returned if not exactly one row is found. Get replaces the legacy GetXxxByID methods we have on projections. However, a list of flexible conditions can be passed. - Operations like UPDATE and DELETE should often only affect a single row, identified by the primary key. If the primary key conditions are missing an error is returned. Because there is no "type safety" enforced here, the errors are returned during runtime. When all possible combinations of API arguments are not properly tested such errors are actually bugs in the experience of the end-user. (The API should do it's own input validation, but a programmer's error is easily made). # How the Problems Are Solved Add `PrimaryKeyColumns` and `PrimaryKeyCondition` to existing repository intefaces. # Additional Changes - Where reducers already used repositories, the PrimaryKeyConditions are used where possible. # Additional Context - Part of https://github.com/zitadel/zitadel/wiki/Decision-Log#expose-primary-key-definition-in-repository-interfaces --------- Co-authored-by: Marco A. <marco@zitadel.com>
This commit is contained in:
@@ -296,6 +296,8 @@ type idProviderColumns interface {
|
||||
}
|
||||
|
||||
type idProviderConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, idpID string) database.Condition
|
||||
InstanceIDCondition(id string) database.Condition
|
||||
OrgIDCondition(id *string) database.Condition
|
||||
IDCondition(id string) IDPIdentifierCondition
|
||||
@@ -327,6 +329,8 @@ type idProviderChanges interface {
|
||||
}
|
||||
|
||||
type IDProviderRepository interface {
|
||||
Repository
|
||||
|
||||
idProviderColumns
|
||||
idProviderConditions
|
||||
idProviderChanges
|
||||
|
||||
@@ -65,6 +65,8 @@ type instanceColumns interface {
|
||||
|
||||
// instanceConditions define all the conditions for the instance table.
|
||||
type instanceConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID string) database.Condition
|
||||
// IDCondition returns an equal filter on the id field.
|
||||
IDCondition(instanceID string) database.Condition
|
||||
// NameCondition returns a filter on the name field.
|
||||
@@ -93,6 +95,8 @@ type instanceChanges interface {
|
||||
|
||||
// InstanceRepository is the interface for the instance repository.
|
||||
type InstanceRepository interface {
|
||||
Repository
|
||||
|
||||
instanceColumns
|
||||
instanceConditions
|
||||
instanceChanges
|
||||
|
||||
@@ -47,6 +47,8 @@ type instanceDomainColumns interface {
|
||||
|
||||
type instanceDomainConditions interface {
|
||||
domainConditions
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(domain string) database.Condition
|
||||
// TypeCondition returns a filter for the type field.
|
||||
TypeCondition(typ DomainType) database.Condition
|
||||
}
|
||||
@@ -58,6 +60,8 @@ type instanceDomainChanges interface {
|
||||
}
|
||||
|
||||
type InstanceDomainRepository interface {
|
||||
Repository
|
||||
|
||||
instanceDomainColumns
|
||||
instanceDomainConditions
|
||||
instanceDomainChanges
|
||||
|
||||
@@ -51,8 +51,10 @@ type organizationColumns interface {
|
||||
|
||||
// organizationConditions define all the conditions for the instance table.
|
||||
type organizationConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, organizationID string) database.Condition
|
||||
// IDCondition returns an equal filter on the id field.
|
||||
IDCondition(instanceID string) database.Condition
|
||||
IDCondition(orgID string) database.Condition
|
||||
// NameCondition returns a filter on the name field.
|
||||
NameCondition(op database.TextOperation, name string) database.Condition
|
||||
// InstanceIDCondition returns a filter on the instance id field.
|
||||
@@ -77,6 +79,8 @@ type organizationChanges interface {
|
||||
|
||||
// OrganizationRepository is the interface for the instance repository.
|
||||
type OrganizationRepository interface {
|
||||
Repository
|
||||
|
||||
organizationColumns
|
||||
organizationConditions
|
||||
organizationChanges
|
||||
|
||||
@@ -47,6 +47,9 @@ type organizationDomainColumns interface {
|
||||
|
||||
type organizationDomainConditions interface {
|
||||
domainConditions
|
||||
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, orgID, domain string) database.Condition
|
||||
// OrgIDCondition returns a filter on the org id field.
|
||||
OrgIDCondition(orgID string) database.Condition
|
||||
// IsVerifiedCondition returns a filter on the is verified field.
|
||||
@@ -65,6 +68,8 @@ type organizationDomainChanges interface {
|
||||
//go:generate mockgen -typed -package domainmock -destination ./mock/org_domain.mock.go . OrganizationDomainRepository
|
||||
|
||||
type OrganizationDomainRepository interface {
|
||||
Repository
|
||||
|
||||
organizationDomainColumns
|
||||
organizationDomainConditions
|
||||
organizationDomainChanges
|
||||
|
||||
@@ -30,8 +30,6 @@ type Project struct {
|
||||
}
|
||||
|
||||
type projectColumns interface {
|
||||
// PrimaryKeyColumns returns the columns for the primary key fields
|
||||
PrimaryKeyColumns() []database.Column
|
||||
// InstanceIDColumn returns the column for the instance id field
|
||||
InstanceIDColumn() database.Column
|
||||
// OrganizationIDColumn returns the column for the organization id field
|
||||
@@ -94,6 +92,8 @@ type projectChanges interface {
|
||||
//
|
||||
//go:generate mockgen -typed -package domainmock -destination ./mock/project.mock.go . ProjectRepository
|
||||
type ProjectRepository interface {
|
||||
Repository
|
||||
|
||||
projectColumns
|
||||
projectConditions
|
||||
projectChanges
|
||||
|
||||
9
backend/v3/domain/repository.go
Normal file
9
backend/v3/domain/repository.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
|
||||
// Repository is the base interface for all repositories.
|
||||
type Repository interface {
|
||||
// PrimaryKeyColumns returns the columns for the primary key fields
|
||||
PrimaryKeyColumns() []database.Column
|
||||
}
|
||||
@@ -27,6 +27,8 @@ type userColumns interface {
|
||||
|
||||
// userConditions define all the conditions for the user table.
|
||||
type userConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, userID string) database.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.
|
||||
@@ -53,6 +55,8 @@ type userChanges interface {
|
||||
|
||||
// UserRepository is the interface for the user repository.
|
||||
type UserRepository interface {
|
||||
Repository
|
||||
|
||||
userColumns
|
||||
userConditions
|
||||
userChanges
|
||||
|
||||
@@ -431,6 +431,14 @@ func (i *idProvider) GetSAML(ctx context.Context, client database.QueryExecutor,
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (i idProvider) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
i.InstanceIDColumn(),
|
||||
i.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (idProvider) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn("identity_providers", "instance_id")
|
||||
}
|
||||
@@ -499,6 +507,13 @@ func (idProvider) UpdatedAtColumn() database.Column {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (i idProvider) PrimaryKeyCondition(instanceID, id string) database.Condition {
|
||||
return database.And(
|
||||
i.InstanceIDCondition(instanceID),
|
||||
i.IDCondition(id),
|
||||
)
|
||||
}
|
||||
|
||||
func (i idProvider) InstanceIDCondition(id string) database.Condition {
|
||||
return database.NewTextCondition(i.InstanceIDColumn(), database.TextOperationEqual, id)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,10 @@ func (i instance) SetConsoleAppID(id string) database.Change {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (i instance) PrimaryKeyCondition(instanceID string) database.Condition {
|
||||
return i.IDCondition(instanceID)
|
||||
}
|
||||
|
||||
// IDCondition implements [domain.instanceConditions].
|
||||
func (i instance) IDCondition(id string) database.Condition {
|
||||
return database.NewTextCondition(i.IDColumn(), database.TextOperationEqual, id)
|
||||
@@ -204,6 +208,11 @@ func (i instance) ExistsDomain(cond database.Condition) database.Condition {
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (i instance) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{i.IDColumn()}
|
||||
}
|
||||
|
||||
// IDColumn implements [domain.instanceColumns].
|
||||
func (i instance) IDColumn() database.Column {
|
||||
return database.NewColumn(i.unqualifiedTableName(), "id")
|
||||
|
||||
@@ -136,6 +136,10 @@ func (i instanceDomain) SetType(typ domain.DomainType) database.Change {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (i instanceDomain) PrimaryKeyCondition(domain string) database.Condition {
|
||||
return i.DomainCondition(database.TextOperationEqual, domain)
|
||||
}
|
||||
|
||||
// DomainCondition implements [domain.InstanceDomainRepository].
|
||||
func (i instanceDomain) DomainCondition(op database.TextOperation, domain string) database.Condition {
|
||||
return database.NewTextCondition(i.DomainColumn(), op, domain)
|
||||
@@ -160,6 +164,13 @@ func (i instanceDomain) TypeCondition(typ domain.DomainType) database.Condition
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (i instanceDomain) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
i.DomainColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreatedAtColumn implements [domain.InstanceDomainRepository].
|
||||
func (i instanceDomain) CreatedAtColumn() database.Column {
|
||||
return database.NewColumn(i.unqualifiedTableName(), "created_at")
|
||||
|
||||
@@ -148,6 +148,13 @@ func (o org) SetState(state domain.OrgState) database.Change {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (o org) PrimaryKeyCondition(instanceID, orgID string) database.Condition {
|
||||
return database.And(
|
||||
o.InstanceIDCondition(instanceID),
|
||||
o.IDCondition(orgID),
|
||||
)
|
||||
}
|
||||
|
||||
// IDCondition implements [domain.organizationConditions].
|
||||
func (o org) IDCondition(id string) database.Condition {
|
||||
return database.NewTextCondition(o.IDColumn(), database.TextOperationEqual, id)
|
||||
@@ -221,6 +228,14 @@ func (o org) ExistsMetadata(cond database.Condition) database.Condition {
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (o org) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
o.InstanceIDColumn(),
|
||||
o.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// IDColumn implements [domain.organizationColumns].
|
||||
func (o org) IDColumn() database.Column {
|
||||
return database.NewColumn(o.unqualifiedTableName(), "id")
|
||||
|
||||
@@ -157,6 +157,14 @@ func (o orgDomain) SetUpdatedAt(updatedAt time.Time) database.Change {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (o orgDomain) PrimaryKeyCondition(instanceID, orgID, domain string) database.Condition {
|
||||
return database.And(
|
||||
o.InstanceIDCondition(instanceID),
|
||||
o.OrgIDCondition(orgID),
|
||||
o.DomainCondition(database.TextOperationEqual, domain),
|
||||
)
|
||||
}
|
||||
|
||||
// DomainCondition implements [domain.OrganizationDomainRepository].
|
||||
func (o orgDomain) DomainCondition(op database.TextOperation, domain string) database.Condition {
|
||||
return database.NewTextCondition(o.DomainColumn(), op, domain)
|
||||
@@ -187,6 +195,15 @@ func (o orgDomain) OrgIDCondition(orgID string) database.Condition {
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (o orgDomain) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
o.InstanceIDColumn(),
|
||||
o.OrgIDColumn(),
|
||||
o.DomainColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreatedAtColumn implements [domain.OrganizationDomainRepository].
|
||||
// Subtle: this method shadows the method ([domain.OrganizationRepository]).CreatedAtColumn of orgDomain.org.
|
||||
func (o orgDomain) CreatedAtColumn() database.Column {
|
||||
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
@@ -29,14 +30,10 @@ func checkRestrictingColumns(
|
||||
return nil
|
||||
}
|
||||
|
||||
type pkRepository interface {
|
||||
PrimaryKeyColumns() []database.Column
|
||||
}
|
||||
|
||||
// checkPKCondition checks if the Primary Key columns are part of the condition.
|
||||
// This can ensure only a single row is affected by updates and deletes.
|
||||
func checkPKCondition(
|
||||
repo pkRepository,
|
||||
repo domain.Repository,
|
||||
condition database.Condition,
|
||||
) error {
|
||||
return checkRestrictingColumns(
|
||||
|
||||
@@ -135,6 +135,13 @@ func (u user) SetUsername(username string) database.Change {
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u user) PrimaryKeyCondition(instanceID, userID string) database.Condition {
|
||||
return database.And(
|
||||
u.InstanceIDCondition(instanceID),
|
||||
u.IDCondition(userID),
|
||||
)
|
||||
}
|
||||
|
||||
// InstanceIDCondition implements [domain.userConditions].
|
||||
func (u user) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
@@ -182,6 +189,14 @@ func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Tim
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (u user) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
u.InstanceIDColumn(),
|
||||
u.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// InstanceIDColumn implements [domain.userColumns].
|
||||
func (user) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn("users", "instance_id")
|
||||
|
||||
@@ -92,8 +92,8 @@ func (p *instanceDomainRelationalProjection) reduceDomainPrimarySet(event events
|
||||
|
||||
_, err := domainRepo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.PrimaryKeyCondition(e.Domain),
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
domainRepo.TypeCondition(domain.DomainTypeCustom),
|
||||
),
|
||||
domainRepo.SetPrimary(),
|
||||
@@ -116,8 +116,8 @@ func (p *instanceDomainRelationalProjection) reduceCustomDomainRemoved(event eve
|
||||
domainRepo := repository.InstanceDomainRepository()
|
||||
_, err := domainRepo.Remove(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.PrimaryKeyCondition(e.Domain),
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
domainRepo.TypeCondition(domain.DomainTypeCustom),
|
||||
),
|
||||
)
|
||||
@@ -158,8 +158,8 @@ func (p *instanceDomainRelationalProjection) reduceTrustedDomainRemoved(event ev
|
||||
domainRepo := repository.InstanceDomainRepository()
|
||||
_, err := domainRepo.Remove(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.PrimaryKeyCondition(e.Domain),
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
domainRepo.TypeCondition(domain.DomainTypeTrusted),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
old_domain "github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -87,11 +86,7 @@ func (p *orgDomainRelationalProjection) reducePrimarySet(event eventstore.Event)
|
||||
}
|
||||
domainRepo := repository.OrganizationDomainRepository()
|
||||
_, err := domainRepo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
),
|
||||
domainRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ResourceOwner, e.Domain),
|
||||
domainRepo.SetPrimary(),
|
||||
domainRepo.SetUpdatedAt(e.CreationDate()),
|
||||
)
|
||||
@@ -111,11 +106,7 @@ func (p *orgDomainRelationalProjection) reduceRemoved(event eventstore.Event) (*
|
||||
}
|
||||
domainRepo := repository.OrganizationDomainRepository()
|
||||
_, err := domainRepo.Remove(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
),
|
||||
domainRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ResourceOwner, e.Domain),
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
@@ -143,11 +134,7 @@ func (p *orgDomainRelationalProjection) reduceVerificationAdded(event eventstore
|
||||
domainRepo := repository.OrganizationDomainRepository()
|
||||
|
||||
_, err := domainRepo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
),
|
||||
domainRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ResourceOwner, e.Domain),
|
||||
domainRepo.SetValidationType(validationType),
|
||||
domainRepo.SetUpdatedAt(e.CreationDate()),
|
||||
)
|
||||
@@ -168,11 +155,7 @@ func (p *orgDomainRelationalProjection) reduceVerified(event eventstore.Event) (
|
||||
domainRepo := repository.OrganizationDomainRepository()
|
||||
|
||||
_, err := domainRepo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
database.And(
|
||||
domainRepo.InstanceIDCondition(e.Aggregate().InstanceID),
|
||||
domainRepo.OrgIDCondition(e.Aggregate().ResourceOwner),
|
||||
domainRepo.DomainCondition(database.TextOperationEqual, e.Domain),
|
||||
),
|
||||
domainRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ResourceOwner, e.Domain),
|
||||
domainRepo.SetVerified(),
|
||||
domainRepo.SetUpdatedAt(e.CreationDate()),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user