mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-19 14:12:52 +00:00
# Which Problems Are Solved This PR implements the endpoints listed in #10445 using relational tables. - [x] UpdateOrganization - [x] ListOrganizations - [x] DeleteOrganization - [x] DeactivateOrganization - [x] ActivateOrganization # How the Problems Are Solved - **UpdateOrganization:** Implemented logic for `Validate()`, `Execute()` and `Events()`. On CQRS side, updating an organization emits events related to the organization and its domains. Separate commanders have been made for the domain events so that their logic is run accordingly and we keep separation of concerns. Implementation of the domains is left to its own ticket. Because some domain-related data is changed during the organization update, its original values are saved as pointers inside [UpdateOrgCommand](https://github.com/zitadel/zitadel/blob/4f87cd8d0cfd75f290aa13c342e4939cecebef73/backend/v3/domain/org_update.go#L20). These variables will be then used as input for the domain commanders (that will run after the update organization one). - **ListOrganizations:** Implemented logic for `Validate()`, `Execute()`. Utility methods have been made to parse the input queries. This commander is used both by the v2 and v2beta endpoint, so a translation layer has been put in place to translate all v2beta requests into v2. The commander strictly handles v2 request only. The translation layer between v2beta and v2 APIs was made so to make it simple to remove it once the v2beta APIs will be removed (a simple deletion should suffice). A `converter` package [is shipped](https://github.com/zitadel/zitadel/blob/4f87cd8d0cfd75f290aa13c342e4939cecebef73/backend/v3/api/org/v2/convert/README.md#L5) as well with instruction for removal. TODOs have been put in place all over the code to indicate that the related piece of code needs to be removed once v2beta -> v2 transition is complete. - **DeleteOrganization:** Implemented logic for `Validate()`, `Execute()` and `Events()`. The `Events()` method is mostly incomplete as it requires a lot of data that is currently not retrievable due to the lack of their relational tables. - **DeactivateOrganization** and **ActivateOrganization**: Implemented logic for `Validate()`, `Execute()` and `Events()`. No notable remarks. 👉 All endpoints return matching errors to the CQRS counterpart: this way allows re-using the integration tests and 👉 All endpoints have been unit-tested through DB mocking 👉 A bunch of TODOs have been put in place for future reworks that are needed (e.g. `Update()` method on repository should also return a timestamp) # Additional Changes - A test utility method to allow mocking query options have been made. This was necessary because `database.QueryOption` is a function and the only comparison possible with functions is `nil` check. So, to mock this, `database.QueryOption` is converted first to `database.QueryOpts`. See 2276742adaef1998ea4844e1f0dca2079e45ccfb - A `BaseCommand` has been created with the idea to collect methods that are going to be needed by other commands. For the moment, it only offers a method to convert query text operations from gRPC to `domain` model. See bb85456dd101282fa76d2e2ad165eed4c8ef61b9 - SQL operations have been reworked to allow for queries using `ContainsIgnoreCase`, `EndsWithIgnoreCase` and other `IgnoreCase` operations. This change is pending approval and might be reworked. See https://github.com/zitadel/zitadel/pull/10704#discussion_r2392886177 # Additional Context `UpdateOrganization` is partially working because the logic for handling domain updates is not done (there is only a draft of the commanders to showcase the call chain). See https://github.com/zitadel/zitadel/pull/10704#discussion_r2356143178 Permissions are not implemented as they are missing. See https://github.com/zitadel/zitadel/pull/10771 - Closes: #10445 --------- Co-authored-by: Silvan <[email protected]>
349 lines
8.5 KiB
Go
349 lines
8.5 KiB
Go
package database
|
|
|
|
import "go.uber.org/mock/gomock"
|
|
|
|
// Condition represents a SQL condition.
|
|
// Its written after the WHERE keyword in a SQL statement.
|
|
type Condition interface {
|
|
gomock.Matcher
|
|
Write(builder *StatementBuilder)
|
|
// IsRestrictingColumn is used to check if the condition filters for a specific column.
|
|
// It acts as a safeguard database operations that should be specific on the given column.
|
|
IsRestrictingColumn(col Column) bool
|
|
}
|
|
|
|
type and struct {
|
|
conditions []Condition
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (a *and) Matches(x any) bool {
|
|
toMatch, ok := x.(*and)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if len(a.conditions) != len(toMatch.conditions) {
|
|
return false
|
|
}
|
|
for i, condition := range a.conditions {
|
|
if !condition.Matches(toMatch.conditions[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (a *and) String() string {
|
|
return "database.and"
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (a and) Write(builder *StatementBuilder) {
|
|
if len(a.conditions) > 1 {
|
|
builder.WriteString("(")
|
|
defer builder.WriteString(")")
|
|
}
|
|
for i, condition := range a.conditions {
|
|
if i > 0 {
|
|
builder.WriteString(" AND ")
|
|
}
|
|
condition.Write(builder)
|
|
}
|
|
}
|
|
|
|
// And combines multiple conditions with AND.
|
|
func And(conditions ...Condition) *and {
|
|
return &and{conditions: conditions}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (a and) IsRestrictingColumn(col Column) bool {
|
|
for _, condition := range a.conditions {
|
|
if condition.IsRestrictingColumn(col) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*and)(nil)
|
|
|
|
type or struct {
|
|
conditions []Condition
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (o *or) Matches(x any) bool {
|
|
toMatch, ok := x.(*or)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if len(o.conditions) != len(toMatch.conditions) {
|
|
return false
|
|
}
|
|
for i, condition := range o.conditions {
|
|
if !condition.Matches(toMatch.conditions[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (o *or) String() string {
|
|
return "database.or"
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (o or) Write(builder *StatementBuilder) {
|
|
if len(o.conditions) > 1 {
|
|
builder.WriteString("(")
|
|
defer builder.WriteString(")")
|
|
}
|
|
for i, condition := range o.conditions {
|
|
if i > 0 {
|
|
builder.WriteString(" OR ")
|
|
}
|
|
condition.Write(builder)
|
|
}
|
|
}
|
|
|
|
// Or combines multiple conditions with OR.
|
|
func Or(conditions ...Condition) *or {
|
|
return &or{conditions: conditions}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns true only if all conditions are restricting the given column.
|
|
func (o or) IsRestrictingColumn(col Column) bool {
|
|
for _, condition := range o.conditions {
|
|
if !condition.IsRestrictingColumn(col) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var _ Condition = (*or)(nil)
|
|
|
|
type isNull struct {
|
|
column Column
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (i *isNull) Matches(x any) bool {
|
|
toMatch, ok := x.(*isNull)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return i.column.Matches(toMatch.column)
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (i *isNull) String() string {
|
|
return "database.isNull"
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (i isNull) Write(builder *StatementBuilder) {
|
|
i.column.WriteQualified(builder)
|
|
builder.WriteString(" IS NULL")
|
|
}
|
|
|
|
// IsNull creates a condition that checks if a column is NULL.
|
|
func IsNull(column Column) *isNull {
|
|
return &isNull{column: column}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns false because it cannot be used for restricting a column.
|
|
func (i isNull) IsRestrictingColumn(col Column) bool {
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*isNull)(nil)
|
|
|
|
type isNotNull struct {
|
|
column Column
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (i *isNotNull) Matches(x any) bool {
|
|
toMatch, ok := x.(*isNotNull)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return i.column.Matches(toMatch.column)
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (i *isNotNull) String() string {
|
|
return "database.isNotNull"
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (i isNotNull) Write(builder *StatementBuilder) {
|
|
i.column.WriteQualified(builder)
|
|
builder.WriteString(" IS NOT NULL")
|
|
}
|
|
|
|
// IsNotNull creates a condition that checks if a column is NOT NULL.
|
|
func IsNotNull(column Column) *isNotNull {
|
|
return &isNotNull{column: column}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns false because it cannot be used for restricting a column.
|
|
func (i isNotNull) IsRestrictingColumn(col Column) bool {
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*isNotNull)(nil)
|
|
|
|
type valueCondition struct {
|
|
write func(builder *StatementBuilder)
|
|
col Column
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (c valueCondition) Matches(x any) bool {
|
|
toMatch, ok := x.(valueCondition)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !c.col.Matches(toMatch.col) {
|
|
return false
|
|
}
|
|
var expectedStatement, inputStatement StatementBuilder
|
|
c.write(&expectedStatement)
|
|
toMatch.write(&inputStatement)
|
|
return expectedStatement.String() == inputStatement.String()
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (c valueCondition) String() string {
|
|
return "database.valueCondition"
|
|
}
|
|
|
|
// NewTextCondition creates a condition that compares a text column with a value.
|
|
// if `op.IsIgnoreCaseOperation()` is true the response is created using [NewTextIgnoreCaseCondition]
|
|
func NewTextCondition[T Text](col Column, op TextOperation, value T) Condition {
|
|
if op.IsIgnoreCaseOperation() {
|
|
return NewTextIgnoreCaseCondition(col, op.Value(), value)
|
|
}
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeTextOperation[T](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewTextIgnoreCaseCondition creates a condition that compares a text column with a value, ignoring case by lowercasing both.
|
|
func NewTextIgnoreCaseCondition[T Text](col Column, op TextOperation, value T) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeTextOperation[T](builder, LowerColumn(col), op, LowerValue(value))
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewDateCondition creates a condition that compares a numeric column with a value.
|
|
func NewNumberCondition[V Number](col Column, op NumberOperation, value V) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeNumberOperation[V](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewDateCondition creates a condition that compares a boolean column with a value.
|
|
func NewBooleanCondition[V Boolean](col Column, value V) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeBooleanOperation[V](builder, col, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewBytesCondition creates a condition that compares a BYTEA column with a value.
|
|
func NewBytesCondition[V Bytes](col Column, op BytesOperation, value any) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeBytesOperation[V](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewColumnCondition creates a condition that compares two columns on equality.
|
|
func NewColumnCondition(col1, col2 Column) Condition {
|
|
return valueCondition{
|
|
col: col1,
|
|
write: func(builder *StatementBuilder) {
|
|
col1.WriteQualified(builder)
|
|
builder.WriteString(" = ")
|
|
col2.WriteQualified(builder)
|
|
},
|
|
}
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (c valueCondition) Write(builder *StatementBuilder) {
|
|
c.write(builder)
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (i valueCondition) IsRestrictingColumn(col Column) bool {
|
|
return i.col.Equals(col)
|
|
}
|
|
|
|
var _ Condition = (*valueCondition)(nil)
|
|
|
|
// existsCondition is a helper to write an EXISTS (SELECT 1 FROM <table> WHERE <condition>) clause.
|
|
// It implements [Condition] so it can be composed with other conditions using And/Or.
|
|
type existsCondition struct {
|
|
table string
|
|
condition Condition
|
|
}
|
|
|
|
// Matches implements [Condition].
|
|
func (e *existsCondition) Matches(x any) bool {
|
|
// Unimplemented
|
|
return false
|
|
}
|
|
|
|
// String implements [Condition].
|
|
func (e *existsCondition) String() string {
|
|
return "unimplemented"
|
|
}
|
|
|
|
// Exists creates a condition that checks for the existence of rows in a subquery.
|
|
func Exists(table string, condition Condition) Condition {
|
|
return &existsCondition{
|
|
table: table,
|
|
condition: condition,
|
|
}
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (e existsCondition) Write(builder *StatementBuilder) {
|
|
builder.WriteString(" EXISTS (SELECT 1 FROM ")
|
|
builder.WriteString(e.table)
|
|
builder.WriteString(" WHERE ")
|
|
e.condition.Write(builder)
|
|
builder.WriteString(")")
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (e existsCondition) IsRestrictingColumn(col Column) bool {
|
|
// Forward to the inner condition so safety checks (like instance_id presence) can still work.
|
|
return e.condition.IsRestrictingColumn(col)
|
|
}
|
|
|
|
var _ Condition = (*existsCondition)(nil)
|