feat(settings_relational): adding settings table for relational db (#10769)

# Which Problems Are Solved

As part of https://github.com/zitadel/zitadel/issues/9599 to push to
move to a relational db table model, this is the migration of the
settings

# Additional Changes

I had to add 2 non source code files
`backend/v3/storage/database/events_testing/font.otf` and
`backend/v3/storage/database/events_testing/picture.png`, these are used
as part of the reduce tests, hence why they're sourced inside
`backend/v3/storage/database/events_testing/settings_instance_test.go`

- Closes https://github.com/zitadel/zitadel/issues/10207

---------

Co-authored-by: adlerhurst <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
Co-authored-by: Stefan Benz <[email protected]>
This commit is contained in:
Iraq
2025-12-02 08:16:21 +01:00
committed by GitHub
co-authored by adlerhurst Copilot Tim Möhlmann Stefan Benz
parent 6c8619e2f0
commit 911967c865
30 changed files with 12316 additions and 27 deletions
-3
View File
@@ -1,12 +1,9 @@
package domain
import (
"errors"
"fmt"
)
var ErrNoAdminSpecified = errors.New("at least one admin must be specified")
type wrongIDPTypeError struct {
expected IDPType
got string
+430
View File
@@ -0,0 +1,430 @@
package domain
import (
"context"
"net/url"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
db_json "github.com/zitadel/zitadel/backend/v3/storage/database/json"
)
//go:generate enumer -type SettingType -transform snake -trimprefix SettingType -sql
type SettingType uint8
const (
SettingTypeLogin SettingType = iota
SettingTypeBranding
SettingTypePasswordComplexity
SettingTypePasswordExpiry
SettingTypeDomain
SettingTypeLockout
SettingTypeSecurity
SettingTypeOrganization
SettingTypeNotification
SettingTypeLegalAndSupport
)
//go:generate enumer -type SettingState -transform snake -trimprefix SettingState -sql
type SettingState int32
const (
SettingStateActive SettingState = iota
SettingStatePreview
)
type Settings struct {
ID string `json:"id,omitempty" db:"id"`
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
OrganizationID *string `json:"organizationId,omitempty" db:"organization_id"`
Type SettingType `json:"type,omitempty" db:"type"`
State SettingState `json:"state,omitempty" db:"state"`
Settings []byte `json:"settings,omitempty" db:"settings"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
}
type settingsColumns interface {
IDColumn() database.Column
InstanceIDColumn() database.Column
OrganizationIDColumn() database.Column
TypeColumn() database.Column
StateColumn() database.Column
SettingsColumn() database.Column
CreatedAtColumn() database.Column
UpdatedAtColumn() database.Column
PrimaryKeyColumns() []database.Column
UniqueColumns() []database.Column
}
type settingsConditions interface {
InstanceIDCondition(id string) database.Condition
OrganizationIDCondition(id *string) database.Condition
IDCondition(id string) database.Condition
PrimaryKeyCondition(instanceID, id string) database.Condition
UniqueCondition(instanceID string, orgID *string, typ SettingType, state SettingState) database.Condition
}
type settingsChanges interface {
// SetSettings(settings string) database.Change
SetUpdatedAt(updatedAt *time.Time) database.Change
}
type settingsRepository[T any] interface {
settingsColumns
settingsConditions
settingsChanges
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*T, error)
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*T, error)
Set(ctx context.Context, client database.QueryExecutor, settings *T) error
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
}
type PasswordlessType int32
const (
PasswordlessTypeNotAllowed PasswordlessType = iota
PasswordlessTypeAllowed
)
type MultiFactorType int32
const (
MultiFactorTypeUnspecified MultiFactorType = iota
MultiFactorTypeU2FWithPIN
)
type SecondFactorType int32
const (
SecondFactorTypeUnspecified SecondFactorType = iota
SecondFactorTypeTOTP
SecondFactorTypeU2F
SecondFactorTypeOTPEmail
SecondFactorTypeOTPSMS
)
type loginSettingsJSONChanges interface {
SetSettingFields(value LoginSettingsAttributes) database.Change
SetAllowUserNamePassword(value bool) db_json.JsonUpdate
SetAllowRegister(value bool) db_json.JsonUpdate
SetAllowExternalIDP(value bool) db_json.JsonUpdate
SetForceMFA(value bool) db_json.JsonUpdate
SetForceMFALocalOnly(value bool) db_json.JsonUpdate
SetHidePasswordReset(value bool) db_json.JsonUpdate
SetIgnoreUnknownUsernames(value bool) db_json.JsonUpdate
SetAllowDomainDiscovery(value bool) db_json.JsonUpdate
SetDisableLoginWithEmail(value bool) db_json.JsonUpdate
SetDisableLoginWithPhone(value bool) db_json.JsonUpdate
SetPasswordlessType(value PasswordlessType) db_json.JsonUpdate
SetDefaultRedirectURI(value string) db_json.JsonUpdate
SetPasswordCheckLifetime(value time.Duration) db_json.JsonUpdate
SetExternalLoginCheckLifetime(value time.Duration) db_json.JsonUpdate
SetMFAInitSkipLifetime(value time.Duration) db_json.JsonUpdate
SetSecondFactorCheckLifetime(value time.Duration) db_json.JsonUpdate
SetMultiFactorCheckLifetime(value time.Duration) db_json.JsonUpdate
AddMFAType(value MultiFactorType) database.Change
RemoveMFAType(value MultiFactorType) database.Change
AddSecondFactorTypes(value SecondFactorType) database.Change
RemoveSecondFactorTypes(value SecondFactorType) database.Change
}
type LoginSettings struct {
Settings
LoginSettingsAttributes
}
type LoginSettingsAttributes struct {
AllowUserNamePassword *bool `json:"allowUsernamePassword,omitempty"`
AllowRegister *bool `json:"allowRegister,omitempty"`
AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"`
ForceMFA *bool `json:"forceMfa,omitempty"`
ForceMFALocalOnly *bool `json:"forceMfaLocalOnly,omitempty"`
HidePasswordReset *bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"`
AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"`
DisableLoginWithEmail *bool `json:"disableLoginWithEmail,omitempty"`
DisableLoginWithPhone *bool `json:"disableLoginWithPhone,omitempty"`
PasswordlessType *PasswordlessType `json:"passwordlessType,omitempty"`
DefaultRedirectURI *string `json:"defaultRedirectUri,omitempty"`
PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"`
ExternalLoginCheckLifetime *time.Duration `json:"externalLoginCheckLifetime,omitempty"`
MFAInitSkipLifetime *time.Duration `json:"mfaInitSkipLifetime,omitempty"`
SecondFactorCheckLifetime *time.Duration `json:"secondFactorCheckLifetime,omitempty"`
MultiFactorCheckLifetime *time.Duration `json:"multiFactorCheckLifetime,omitempty"`
MFAType []MultiFactorType `json:"mfaType,omitempty"`
SecondFactorTypes []SecondFactorType `json:"secondFactors,omitempty"`
}
type LoginSettingsRepository interface {
settingsRepository[LoginSettings]
loginSettingsJSONChanges
SetColumns(ctx context.Context, client database.QueryExecutor, settings *Settings, changes ...database.Change) error
}
type BrandingPolicyThemeMode int32
const (
BrandingPolicyThemeAuto BrandingPolicyThemeMode = iota
BrandingPolicyThemeLight
BrandingPolicyThemeDark
)
type brandingSettingsJSONChanges interface {
SetSettingFields(value BrandingSettingsAttributes) database.Change
SetPrimaryColorLight(value string) db_json.JsonUpdate
SetBackgroundColorLight(value string) db_json.JsonUpdate
SetWarnColorLight(value string) db_json.JsonUpdate
SetFontColorLight(value string) db_json.JsonUpdate
SetLogoURLLight(value url.URL) db_json.JsonUpdate
SetIconURLLight(value url.URL) db_json.JsonUpdate
SetPrimaryColorDark(value string) db_json.JsonUpdate
SetBackgroundColorDark(value string) db_json.JsonUpdate
SetWarnColorDark(value string) db_json.JsonUpdate
SetFontColorDark(value string) db_json.JsonUpdate
SetLogoURLDark(value url.URL) db_json.JsonUpdate
SetIconURLDark(value url.URL) db_json.JsonUpdate
SetHideLoginNameSuffix(value bool) db_json.JsonUpdate
SetErrorMsgPopup(value bool) db_json.JsonUpdate
SetDisableWatermark(value bool) db_json.JsonUpdate
SetThemeMode(value BrandingPolicyThemeMode) db_json.JsonUpdate
SetFontURL(value url.URL) db_json.JsonUpdate
}
type BrandingSettings struct {
Settings
BrandingSettingsAttributes
}
type BrandingSettingsAttributes struct {
PrimaryColorLight *string `json:"primaryColorLight,omitempty"`
BackgroundColorLight *string `json:"backgroundColorLight,omitempty"`
WarnColorLight *string `json:"warnColorLight,omitempty"`
FontColorLight *string `json:"fontColorLight,omitempty"`
LogoURLLight *url.URL `json:"logoUrlLight,omitempty"`
IconURLLight *url.URL `json:"iconUrlLight,omitempty"`
PrimaryColorDark *string `json:"primaryColorDark,omitempty"`
BackgroundColorDark *string `json:"backgroundColorDark,omitempty"`
WarnColorDark *string `json:"warnColorDark,omitempty"`
FontColorDark *string `json:"fontColorDark,omitempty"`
LogoURLDark *url.URL `json:"logoUrlDark,omitempty"`
IconURLDark *url.URL `json:"iconUrlDark,omitempty"`
HideLoginNameSuffix *bool `json:"hideLoginNameSuffix,omitempty"`
ErrorMsgPopup *bool `json:"errorMsgPopup,omitempty"`
DisableWatermark *bool `json:"disableMsgPopup,omitempty"`
ThemeMode *BrandingPolicyThemeMode `json:"themeMode,omitempty"`
FontURL *url.URL `json:"fontUrl,omitempty"`
}
type BrandingSettingsRepository interface {
settingsRepository[BrandingSettings]
brandingSettingsJSONChanges
StateCondition(typ SettingState) database.Condition
Activate(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
ActivateAt(ctx context.Context, client database.QueryExecutor, condition database.Condition, updatedAt time.Time) (int64, error)
SetColumns(ctx context.Context, client database.QueryExecutor, settings *Settings, changes ...database.Change) error
}
type passwordComplexitySettingsJSONChanges interface {
SetSettingFields(value PasswordComplexitySettingsAttributes) database.Change
SetMinLength(value uint64) db_json.JsonUpdate
SetHasLowercase(value bool) db_json.JsonUpdate
SetHasUppercase(value bool) db_json.JsonUpdate
SetHasNumber(value bool) db_json.JsonUpdate
SetHasSymbol(value bool) db_json.JsonUpdate
}
type PasswordComplexitySettings struct {
Settings
PasswordComplexitySettingsAttributes
}
type PasswordComplexitySettingsAttributes struct {
MinLength *uint64 `json:"minLength,omitempty"`
HasLowercase *bool `json:"hasLowercase,omitempty"`
HasUppercase *bool `json:"hasUppercase,omitempty"`
HasNumber *bool `json:"hasNumber,omitempty"`
HasSymbol *bool `json:"hasSymbol,omitempty"`
}
type PasswordComplexitySettingsRepository interface {
settingsRepository[PasswordComplexitySettings]
passwordComplexitySettingsJSONChanges
}
type passwordExpirySettingsJSONChanges interface {
SetSettingFields(value PasswordExpirySettingsAttributes) database.Change
SetExpireWarnDays(value uint64) db_json.JsonUpdate
SetMaxAgeDays(value uint64) db_json.JsonUpdate
}
type PasswordExpirySettings struct {
Settings
PasswordExpirySettingsAttributes
}
type PasswordExpirySettingsAttributes struct {
ExpireWarnDays *uint64 `json:"expireWarnDays,omitempty"`
MaxAgeDays *uint64 `json:"maxAgeDays,omitempty"`
}
type PasswordExpirySettingsRepository interface {
settingsRepository[PasswordExpirySettings]
passwordExpirySettingsJSONChanges
}
type lockoutSettingsJSONChanges interface {
SetSettingFields(value LockoutSettingsAttributes) database.Change
SetMaxPasswordAttempts(value uint64) db_json.JsonUpdate
SetMaxOTPAttempts(value uint64) db_json.JsonUpdate
SetShowLockOutFailures(value bool) db_json.JsonUpdate
}
type LockoutSettings struct {
Settings
LockoutSettingsAttributes
}
type LockoutSettingsAttributes struct {
MaxPasswordAttempts *uint64 `json:"maxPasswordAttempts,omitempty"`
MaxOTPAttempts *uint64 `json:"maxOtpAttempts,omitempty"`
ShowLockOutFailures *bool `json:"showLockOutFailures,omitempty"`
}
type LockoutSettingsRepository interface {
settingsRepository[LockoutSettings]
lockoutSettingsJSONChanges
}
type securitySettingsJSONChanges interface {
SetSettingFields(value SecuritySettingsAttributes) database.Change
SetEnableIframeEmbedding(value bool) db_json.JsonUpdate
SetAllowedOrigins(values []string) db_json.JsonUpdate
SetEnableImpersonation(value bool) db_json.JsonUpdate
}
type SecuritySettings struct {
Settings
SecuritySettingsAttributes
}
type SecuritySettingsAttributes struct {
EnableIframeEmbedding *bool `json:"enableIframeEmbedding,omitempty"`
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
EnableImpersonation *bool `json:"enableImpersonation,omitempty"`
}
type SecuritySettingsRepository interface {
settingsRepository[SecuritySettings]
securitySettingsJSONChanges
}
type domainSettingsJSONChanges interface {
SetSettingFields(value DomainSettingsAttributes) database.Change
SetLoginNameIncludesDomain(value bool) db_json.JsonUpdate
SetRequireOrgDomainVerification(value bool) db_json.JsonUpdate
SetSMTPSenderAddressMatchesInstanceDomain(value bool) db_json.JsonUpdate
}
type DomainSettings struct {
Settings
DomainSettingsAttributes
}
type DomainSettingsAttributes struct {
LoginNameIncludesDomain *bool `json:"loginNameIncludesDomain,omitempty"`
RequireOrgDomainVerification *bool `json:"requireOrgDomainVerification,omitempty"`
SMTPSenderAddressMatchesInstanceDomain *bool `json:"smtpSenderAddressMatchesInstanceDomain,omitempty"`
}
type DomainSettingsRepository interface {
settingsRepository[DomainSettings]
domainSettingsJSONChanges
}
type organizationSettingsJSONChanges interface {
SetSettingFields(value OrganizationSettingsAttributes) database.Change
SetOrganizationScopedUsernames(value bool) db_json.JsonUpdate
}
type OrganizationSettings struct {
Settings
OrganizationSettingsAttributes
}
type OrganizationSettingsAttributes struct {
OrganizationScopedUsernames *bool `json:"organizationScopedUsernames,omitempty"`
}
type OrganizationSettingsRepository interface {
settingsRepository[OrganizationSettings]
organizationSettingsJSONChanges
}
type notificationSettingsJSONChanges interface {
SetSettingFields(value NotificationSettingsAttributes) database.Change
SetPasswordChange(value bool) db_json.JsonUpdate
}
type NotificationSettings struct {
Settings
NotificationSettingsAttributes
}
type NotificationSettingsAttributes struct {
PasswordChange *bool `json:"passwordChange,omitempty"`
}
type NotificationSettingsRepository interface {
settingsRepository[NotificationSettings]
notificationSettingsJSONChanges
}
type legalAndSupportSettingsJSONChanges interface {
SetSettingFields(value LegalAndSupportSettingsAttributes) database.Change
SetTOSLink(value string) db_json.JsonUpdate
SetPrivacyPolicyLink(value string) db_json.JsonUpdate
SetHelpLink(value string) db_json.JsonUpdate
SetSupportEmail(value string) db_json.JsonUpdate
SetDocsLink(value string) db_json.JsonUpdate
SetCustomLink(value string) db_json.JsonUpdate
SetCustomLinkText(value string) db_json.JsonUpdate
}
type LegalAndSupportSettings struct {
Settings
LegalAndSupportSettingsAttributes
}
type LegalAndSupportSettingsAttributes struct {
TOSLink *string `json:"tosLink,omitempty"`
PrivacyPolicyLink *string `json:"privacyPolicyLink,omitempty"`
HelpLink *string `json:"helpLink,omitempty"`
SupportEmail *string `json:"supportEmail,omitempty"`
DocsLink *string `json:"docsLink,omitempty"`
CustomLink *string `json:"customLink,omitempty"`
CustomLinkText *string `json:"customLinkText,omitempty"`
}
type LegalAndSupportSettingsRepository interface {
settingsRepository[LegalAndSupportSettings]
legalAndSupportSettingsJSONChanges
}
+109
View File
@@ -0,0 +1,109 @@
// Code generated by "enumer -type SettingState -transform snake -trimprefix SettingState -sql"; DO NOT EDIT.
package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
const _SettingStateName = "activepreview"
var _SettingStateIndex = [...]uint8{0, 6, 13}
const _SettingStateLowerName = "activepreview"
func (i SettingState) String() string {
if i < 0 || i >= SettingState(len(_SettingStateIndex)-1) {
return fmt.Sprintf("SettingState(%d)", i)
}
return _SettingStateName[_SettingStateIndex[i]:_SettingStateIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _SettingStateNoOp() {
var x [1]struct{}
_ = x[SettingStateActive-(0)]
_ = x[SettingStatePreview-(1)]
}
var _SettingStateValues = []SettingState{SettingStateActive, SettingStatePreview}
var _SettingStateNameToValueMap = map[string]SettingState{
_SettingStateName[0:6]: SettingStateActive,
_SettingStateLowerName[0:6]: SettingStateActive,
_SettingStateName[6:13]: SettingStatePreview,
_SettingStateLowerName[6:13]: SettingStatePreview,
}
var _SettingStateNames = []string{
_SettingStateName[0:6],
_SettingStateName[6:13],
}
// SettingStateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func SettingStateString(s string) (SettingState, error) {
if val, ok := _SettingStateNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _SettingStateNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to SettingState values", s)
}
// SettingStateValues returns all values of the enum
func SettingStateValues() []SettingState {
return _SettingStateValues
}
// SettingStateStrings returns a slice of all String values of the enum
func SettingStateStrings() []string {
strs := make([]string, len(_SettingStateNames))
copy(strs, _SettingStateNames)
return strs
}
// IsASettingState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i SettingState) IsASettingState() bool {
for _, v := range _SettingStateValues {
if i == v {
return true
}
}
return false
}
func (i SettingState) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *SettingState) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of SettingState: %[1]T(%[1]v)", value)
}
val, err := SettingStateString(str)
if err != nil {
return err
}
*i = val
return nil
}
+141
View File
@@ -0,0 +1,141 @@
// Code generated by "enumer -type SettingType -transform snake -trimprefix SettingType -sql"; DO NOT EDIT.
package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
const _SettingTypeName = "loginbrandingpassword_complexitypassword_expirydomainlockoutsecurityorganizationnotificationlegal_and_support"
var _SettingTypeIndex = [...]uint8{0, 5, 13, 32, 47, 53, 60, 68, 80, 92, 109}
const _SettingTypeLowerName = "loginbrandingpassword_complexitypassword_expirydomainlockoutsecurityorganizationnotificationlegal_and_support"
func (i SettingType) String() string {
if i >= SettingType(len(_SettingTypeIndex)-1) {
return fmt.Sprintf("SettingType(%d)", i)
}
return _SettingTypeName[_SettingTypeIndex[i]:_SettingTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _SettingTypeNoOp() {
var x [1]struct{}
_ = x[SettingTypeLogin-(0)]
_ = x[SettingTypeBranding-(1)]
_ = x[SettingTypePasswordComplexity-(2)]
_ = x[SettingTypePasswordExpiry-(3)]
_ = x[SettingTypeDomain-(4)]
_ = x[SettingTypeLockout-(5)]
_ = x[SettingTypeSecurity-(6)]
_ = x[SettingTypeOrganization-(7)]
_ = x[SettingTypeNotification-(8)]
_ = x[SettingTypeLegalAndSupport-(9)]
}
var _SettingTypeValues = []SettingType{SettingTypeLogin, SettingTypeBranding, SettingTypePasswordComplexity, SettingTypePasswordExpiry, SettingTypeDomain, SettingTypeLockout, SettingTypeSecurity, SettingTypeOrganization, SettingTypeNotification, SettingTypeLegalAndSupport}
var _SettingTypeNameToValueMap = map[string]SettingType{
_SettingTypeName[0:5]: SettingTypeLogin,
_SettingTypeLowerName[0:5]: SettingTypeLogin,
_SettingTypeName[5:13]: SettingTypeBranding,
_SettingTypeLowerName[5:13]: SettingTypeBranding,
_SettingTypeName[13:32]: SettingTypePasswordComplexity,
_SettingTypeLowerName[13:32]: SettingTypePasswordComplexity,
_SettingTypeName[32:47]: SettingTypePasswordExpiry,
_SettingTypeLowerName[32:47]: SettingTypePasswordExpiry,
_SettingTypeName[47:53]: SettingTypeDomain,
_SettingTypeLowerName[47:53]: SettingTypeDomain,
_SettingTypeName[53:60]: SettingTypeLockout,
_SettingTypeLowerName[53:60]: SettingTypeLockout,
_SettingTypeName[60:68]: SettingTypeSecurity,
_SettingTypeLowerName[60:68]: SettingTypeSecurity,
_SettingTypeName[68:80]: SettingTypeOrganization,
_SettingTypeLowerName[68:80]: SettingTypeOrganization,
_SettingTypeName[80:92]: SettingTypeNotification,
_SettingTypeLowerName[80:92]: SettingTypeNotification,
_SettingTypeName[92:109]: SettingTypeLegalAndSupport,
_SettingTypeLowerName[92:109]: SettingTypeLegalAndSupport,
}
var _SettingTypeNames = []string{
_SettingTypeName[0:5],
_SettingTypeName[5:13],
_SettingTypeName[13:32],
_SettingTypeName[32:47],
_SettingTypeName[47:53],
_SettingTypeName[53:60],
_SettingTypeName[60:68],
_SettingTypeName[68:80],
_SettingTypeName[80:92],
_SettingTypeName[92:109],
}
// SettingTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func SettingTypeString(s string) (SettingType, error) {
if val, ok := _SettingTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _SettingTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to SettingType values", s)
}
// SettingTypeValues returns all values of the enum
func SettingTypeValues() []SettingType {
return _SettingTypeValues
}
// SettingTypeStrings returns a slice of all String values of the enum
func SettingTypeStrings() []string {
strs := make([]string, len(_SettingTypeNames))
copy(strs, _SettingTypeNames)
return strs
}
// IsASettingType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i SettingType) IsASettingType() bool {
for _, v := range _SettingTypeValues {
if i == v {
return true
}
}
return false
}
func (i SettingType) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *SettingType) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of SettingType: %[1]T(%[1]v)", value)
}
val, err := SettingTypeString(str)
if err != nil {
return err
}
*i = val
return nil
}
+18 -9
View File
@@ -12,7 +12,7 @@ import (
type Change interface {
gomock.Matcher
// Write writes the change to the given statement builder.
Write(builder *StatementBuilder)
Write(builder *StatementBuilder) error
// IsOnColumn checks if the change is on the given column.
IsOnColumn(col Column) bool
}
@@ -62,10 +62,11 @@ func NewChangePtr[V Value](col Column, value *V) Change {
}
// Write implements [Change].
func (c change[V]) Write(builder *StatementBuilder) {
func (c change[V]) Write(builder *StatementBuilder) error {
c.column.WriteUnqualified(builder)
builder.WriteString(" = ")
builder.WriteArg(c.value)
return nil
}
// IsOnColumn implements [Change].
@@ -87,13 +88,17 @@ func (c Changes) IsOnColumn(col Column) bool {
}
// Write implements [Change].
func (m Changes) Write(builder *StatementBuilder) {
func (m Changes) Write(builder *StatementBuilder) error {
for i, change := range m {
if i > 0 {
builder.WriteString(", ")
}
change.Write(builder)
err := change.Write(builder)
if err != nil {
return err
}
}
return nil
}
// Matches implements [gomock.Matcher].
@@ -153,10 +158,11 @@ func (c *changeToColumn) String() string {
}
// Write implements [Change].
func (c *changeToColumn) Write(builder *StatementBuilder) {
func (c *changeToColumn) Write(builder *StatementBuilder) error {
c.to.WriteUnqualified(builder)
builder.WriteString(" = ")
c.from.WriteQualified(builder)
return nil
}
var _ Change = (*changeToColumn)(nil)
@@ -191,11 +197,12 @@ func (i *incrementColumnChange) String() string {
}
// Write implements [Change].
func (i *incrementColumnChange) Write(builder *StatementBuilder) {
func (i *incrementColumnChange) Write(builder *StatementBuilder) error {
i.column.WriteUnqualified(builder)
builder.WriteString(" = ")
i.column.WriteUnqualified(builder)
builder.WriteString(" + 1")
return nil
}
var _ Change = (*incrementColumnChange)(nil)
@@ -242,7 +249,7 @@ func (c *changeToStatement) String() string {
}
// Write implements [Change].
func (c *changeToStatement) Write(builder *StatementBuilder) {
func (c *changeToStatement) Write(builder *StatementBuilder) error {
_, ok := c.column.(Columns)
if ok {
builder.WriteRune('(')
@@ -254,6 +261,7 @@ func (c *changeToStatement) Write(builder *StatementBuilder) {
builder.WriteString(" = (")
c.stmt(builder)
builder.WriteString(")")
return nil
}
var _ Change = (*changeToStatement)(nil)
@@ -322,11 +330,12 @@ func (c *cteChange) String() string {
}
// Write implements [CTEChange].
func (c *cteChange) Write(builder *StatementBuilder) {
func (c *cteChange) Write(builder *StatementBuilder) error {
if c.change == nil {
return
return nil
}
c.change(c.name).Write(builder)
return nil
}
// WriteCTE implements [CTEChange].
+2 -1
View File
@@ -64,7 +64,8 @@ func TestChangeWrite(t *testing.T) {
} {
t.Run(test.name, func(t *testing.T) {
var builder StatementBuilder
test.change.Write(&builder)
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.want.stmt, builder.String())
require.Len(t, builder.Args(), len(test.want.args))
for i, arg := range test.want.args {
+2 -4
View File
@@ -157,9 +157,8 @@ func IsNull(column Column) *isNull {
}
// IsRestrictingColumn implements [Condition].
// It returns false because it cannot be used for restricting a column.
func (i isNull) IsRestrictingColumn(col Column) bool {
return false
return true
}
var _ Condition = (*isNull)(nil)
@@ -194,9 +193,8 @@ func IsNotNull(column Column) *isNotNull {
}
// IsRestrictingColumn implements [Condition].
// It returns false because it cannot be used for restricting a column.
func (i isNotNull) IsRestrictingColumn(col Column) bool {
return false
return true
}
var _ Condition = (*isNotNull)(nil)
@@ -51,6 +51,9 @@ func wrapPgError(err *pgconn.PgError) error {
// 23502: not_null_violation - A value violates a NOT NULL constraint.
case "23502":
return database.NewNotNullError(err.TableName, err.ConstraintName, err)
// 22P02: integrity violation - A value is being inserted into a enum that is not valid
case "22P02":
return database.NewCheckError(err.TableName, err.ConstraintName, err)
default:
return database.NewUnknownError(err)
}
@@ -0,0 +1,16 @@
package migration
import (
_ "embed"
)
var (
//go:embed 009_settings_table/up.sql
up009SettingsTable string
//go:embed 009_settings_table/down.sql
down009SettingsTable string
)
func init() {
registerSQLMigration(9, up009SettingsTable, down009SettingsTable)
}
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS zitadel.settings;
DROP TYPE IF EXISTS zitadel.settings_type;
DROP TYPE IF EXISTS zitadel.settings_state;
@@ -0,0 +1,95 @@
CREATE TYPE zitadel.settings_type AS ENUM (
'login',
'branding',
'password_complexity',
'password_expiry',
'domain',
'lockout',
'security',
'organization',
'notification',
'legal_and_support'
);
CREATE TYPE zitadel.settings_state AS ENUM (
'active',
'preview'
);
CREATE TABLE zitadel.settings (
instance_id TEXT NOT NULL
, organization_id TEXT
, id TEXT NOT NULL DEFAULT gen_random_uuid()
, type zitadel.settings_type NOT NULL
, state zitadel.settings_state NOT NULL
, settings JSONB -- the storage does not really care about what is configured so we store it as json
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
, PRIMARY KEY (instance_id, id, type, state)
, FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id) ON DELETE CASCADE
, FOREIGN KEY (instance_id, organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE
, UNIQUE (instance_id, organization_id, type, state)
);
CREATE UNIQUE INDEX idx_settings_unique_type ON zitadel.settings (instance_id, organization_id, type, state) NULLS NOT DISTINCT;
CREATE TRIGGER trigger_set_updated_at
BEFORE UPDATE ON zitadel.settings
FOR EACH ROW
WHEN (NEW.updated_at IS NULL)
EXECUTE FUNCTION zitadel.set_updated_at();
CREATE OR REPLACE FUNCTION zitadel.jsonb_array_remove(
source JSONB
, path TEXT[]
, value anyelement
, outt out jsonb
)
language 'plpgsql'
AS $$
BEGIN
outt = jsonb_set(source, path,
(CASE WHEN (SELECT source ?& path) then
coalesce(
(SELECT jsonb_agg(v)
FROM jsonb_array_elements(source #>path) AS elem(v)
WHERE v::text <> value::text),
jsonb_build_array())
ELSe
jsonb_build_array()
END)::jsonb, TRUE);
END;
$$;
CREATE OR REPLACE FUNCTION zitadel.jsonb_array_append(
source jsonb
, path text[]
, value anyelement
, outt out jsonb
)
language 'plpgsql'
AS $$
BEGIN
outt := jsonb_insert(
source,
(CASE WHEN (SELECT source ?& path) then
array_append(path, '-1')
ELSE
path
END)::text[],
(CASE WHEN (select source ?& path) then
value::TEXT::jsonb
ELSE
jsonb_build_array(value)
END),
TRUE
);
END;
$$;
Binary file not shown.
@@ -0,0 +1,97 @@
<svg width="100%" height="100%" viewBox="0 0 295 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="zitadel-logo-light" transform="matrix(1,0,0,1,-20.9181,-89.5699)">
<rect x="20.918" y="89.57" width="294.943" height="79.632" style="fill:none;"></rect>
<g transform="matrix(2.73883,0,0,1.55076,-35267,23.6366)">
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12622.8,-105.843)">
<path d="M1493.5,1056.38L1493.5,1037L1496.5,1037L1496.5,1061.62L1426.02,1020.38L1496.5,979.392L1496.5,1004L1493.5,1004L1493.5,984.608L1431.98,1020.39L1493.5,1056.38Z" style="fill:white;"></path>
</g>
<g>
<g transform="matrix(-0.0429306,-0.282967,0.160219,-0.0758207,12884.5,137.392)">
<path d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z" style="fill:url(#_Linear1);"></path>
</g>
<g transform="matrix(0.160219,0.0758207,-0.0429306,0.282967,12878.9,10.8747)">
<path d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z" style="fill:url(#_Linear2);"></path>
</g>
<g transform="matrix(-0.117289,0.207146,-0.117289,-0.207146,12943.8,65.7)">
<path d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z" style="fill:url(#_Linear3);"></path>
</g>
<g transform="matrix(-0.160219,-0.0758207,0.0429306,-0.282967,12917.4,132.195)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" style="fill:url(#_Linear4);"></path>
</g>
<g transform="matrix(-0.117289,0.207146,0.117289,0.207146,12897.8,5.87512)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" style="fill:url(#_Linear5);"></path>
</g>
<g transform="matrix(-0.0429306,-0.282967,-0.160219,0.0758207,12936.8,97.6441)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" style="fill:url(#_Linear6);"></path>
</g>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12622.6,-105.767)">
<circle cx="1496" cy="1004" r="7" style="fill:white;"></circle>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12624.1,-96.4295)">
<circle cx="1496" cy="1004" r="7" style="fill:white;"></circle>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12625,-90.2042)">
<circle cx="1496" cy="1004" r="7" style="fill:white;"></circle>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12612.8,-96.1272)">
<circle cx="1496" cy="1004" r="7" style="fill:white;"></circle>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0429306,0.282967,12621.7,-111.993)">
<circle cx="1496" cy="1004" r="7" style="fill:white;"></circle>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0375643,0.247596,12637.2,-10.9628)">
<path d="M1499.26,757.787C1499.26,757.787 1497.37,756.489 1497,755.2C1496.71,754.182 1496.57,750.662 1496.54,750C1496.41,747.303 1499.21,745.644 1499.21,745.644L1490.01,745.835C1490.01,745.835 1493.15,745.713 1493.46,750C1493.51,750.661 1493.23,753.476 1493,755.2C1492.91,756.447 1491.2,757.668 1491.2,757.668L1499.26,757.787Z" style="fill:white;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0375643,0.247596,12639.5,4.60032)">
<path d="M1495,760L1495,744" style="fill:none;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0375643,0.247596,12639.5,4.60032)">
<path d="M1498.27,757.077C1498.27,757.077 1496.71,756.46 1496.65,754.8C1496.65,753.658 1496.64,753.281 1496.65,752.016C1496.62,751.334 1496.59,750.608 1496.65,749.949C1496.78,746.836 1498.5,746.156 1498.5,746.156L1491.46,745.931C1491.46,745.931 1493.37,746.719 1493.65,749.83C1493.71,750.489 1493.69,751.528 1493.65,752.209C1493.64,753.331 1493.64,753.413 1493.65,754.518C1493.68,756.334 1492.58,756.827 1492.58,756.827L1498.27,757.077Z" style="fill:white;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0375643,0.247596,12627.7,-0.733956)">
<path d="M1496.17,759.473L1555.54,720.014" style="fill:none;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,0.0375643,0.247596,12627.7,-0.733956)">
<path d="M1500.86,762.056C1500.86,762.056 1499.86,760.4 1503.09,757.456C1504.91,755.797 1507.33,754.151 1509.98,752.255C1514.82,748.79 1520.68,744.94 1526.52,741.049C1531.45,737.766 1536.38,734.479 1540.82,731.68C1544.52,729.349 1547.85,727.296 1550.54,725.8C1551.07,725.506 1551.6,725.329 1552.05,725.029C1554.73,723.257 1556.85,724.968 1556.85,724.968L1552.23,716.282C1552.23,716.282 1551.99,719.454 1550,720.997C1549.57,721.333 1549.15,721.741 1548.67,722.12C1546.2,724.053 1542.99,726.344 1539.39,728.867C1535.06,731.898 1530.13,735.166 1525.19,738.438C1519.35,742.314 1513.52,746.234 1508.49,749.329C1505.74,751.023 1503.28,752.577 1501.13,753.598C1497.99,755.086 1495.28,753.617 1495.28,753.617L1500.86,762.056Z" style="fill:white;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,-0.0375643,-0.247596,12684.9,376.33)">
<path d="M1496.17,759.473L1555.54,720.014" style="fill:none;"></path>
</g>
<g transform="matrix(0.160219,-0.0758207,-0.0375643,-0.247596,12684.9,376.33)">
<path d="M1496.1,754.362C1496.1,754.362 1497.2,755.607 1501.13,753.598C1503.25,752.509 1505.74,751.023 1508.49,749.329C1513.52,746.234 1519.35,742.314 1525.19,738.438C1530.13,735.166 1534.94,731.832 1539.27,728.802C1542.87,726.279 1549.36,722.059 1549.81,721.75C1552.75,719.73 1552.18,718.196 1552.18,718.196L1555.28,724.152C1555.28,724.152 1553.77,722.905 1551.37,724.681C1550.93,725.006 1544.52,729.349 1540.82,731.68C1536.38,734.479 1531.45,737.766 1526.52,741.049C1520.68,744.94 1514.82,748.79 1509.98,752.255C1507.33,754.151 1504.89,755.771 1503.09,757.456C1499.47,760.841 1501.26,763.283 1501.26,763.283L1496.1,754.362Z" style="fill:white;"></path>
</g>
<g transform="matrix(0.156811,0,0,0.230771,12477,-400.567)">
<g transform="matrix(94.7456,0,0,94.7456,2811.73,2063)">
<path d="M0.449,-0.7L0.177,-0.7C0.185,-0.682 0.197,-0.654 0.2,-0.648C0.205,-0.639 0.216,-0.628 0.239,-0.628L0.32,-0.628C0.332,-0.628 0.336,-0.62 0.334,-0.611L0.128,-0L0.389,-0C0.412,-0 0.422,-0.01 0.427,-0.02L0.45,-0.071L0.255,-0.071C0.245,-0.071 0.239,-0.078 0.242,-0.09L0.449,-0.7Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,2882.79,2063)">
<path d="M0.214,-0.7L0.214,-0.015C0.215,-0.01 0.218,-0 0.235,-0L0.286,-0L0.286,-0.672C0.286,-0.684 0.278,-0.7 0.257,-0.7L0.214,-0.7Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,2944.37,2063)">
<path d="M0.441,-0.7L0.155,-0.7C0.143,-0.7 0.133,-0.69 0.133,-0.678L0.133,-0.629L0.234,-0.629L0.234,-0.015C0.234,-0.01 0.237,-0 0.254,-0L0.305,-0L0.305,-0.612C0.306,-0.621 0.313,-0.629 0.323,-0.629L0.379,-0.629C0.402,-0.629 0.413,-0.639 0.417,-0.648L0.441,-0.7Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,3010.69,2063)">
<path d="M0.422,-0L0.343,-0L0.28,-0.482L0.217,-0L0.138,-0L0.244,-0.7L0.283,-0.7C0.313,-0.7 0.318,-0.681 0.321,-0.662L0.422,-0Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,3077.96,2063)">
<path d="M0.186,-0.7L0.186,-0L0.325,-0C0.374,-0 0.413,-0.039 0.414,-0.088L0.414,-0.612C0.413,-0.661 0.374,-0.7 0.325,-0.7L0.186,-0.7ZM0.343,-0.108C0.343,-0.081 0.325,-0.071 0.305,-0.071L0.258,-0.071L0.258,-0.628L0.305,-0.628C0.325,-0.628 0.343,-0.618 0.343,-0.592L0.343,-0.108Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,3149.02,2063)">
<path d="M0.291,-0.071L0.291,-0.314C0.291,-0.323 0.299,-0.331 0.308,-0.331L0.338,-0.331C0.361,-0.331 0.371,-0.341 0.376,-0.35C0.379,-0.356 0.391,-0.385 0.399,-0.403L0.291,-0.403L0.291,-0.611C0.291,-0.621 0.298,-0.628 0.308,-0.628L0.366,-0.628C0.389,-0.628 0.4,-0.639 0.404,-0.648L0.428,-0.7L0.241,-0.7C0.229,-0.7 0.22,-0.691 0.219,-0.68L0.219,-0L0.379,-0C0.402,-0 0.413,-0.01 0.418,-0.019C0.421,-0.025 0.433,-0.053 0.441,-0.071L0.291,-0.071Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
<g transform="matrix(94.7456,0,0,94.7456,3220.08,2063)">
<path d="M0.283,-0.071L0.283,-0.678C0.283,-0.69 0.273,-0.699 0.261,-0.7L0.211,-0.7L0.211,-0L0.383,-0C0.406,-0 0.417,-0.01 0.422,-0.019C0.425,-0.025 0.437,-0.053 0.445,-0.071L0.283,-0.071Z" style="fill:white;fill-rule:nonzero;"></path>
</g>
</g>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-41.5984,155.247,-155.247,-41.5984,201.516,76.8392)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(155.247,-41.5984,41.5984,155.247,110.08,195.509)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-113.649,-113.649,113.649,-113.649,258.31,215.618)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-155.247,41.5984,-41.5984,-155.247,220.914,144.546)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
<linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-113.649,113.649,113.649,113.649,206.837,124.661)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-41.5984,-155.247,-155.247,41.5984,152.054,262.8)"><stop offset="0" style="stop-color:rgb(255,143,0);stop-opacity:1"></stop><stop offset="1" style="stop-color:rgb(254,0,255);stop-opacity:1"></stop></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
package json
import (
"encoding/json"
"reflect"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
var _ database.Change = (*jsonChanges)(nil)
type JsonUpdate interface {
writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error
getPathValue() ([]string, string, error)
}
type FieldChange struct {
path []string
value any
}
func NewFieldChange(path []string, value any) JsonUpdate {
return &FieldChange{
path: path,
value: value,
}
}
func (c *FieldChange) getPathValue() ([]string, string, error) {
path, v := c.path, c.value
value, err := json.Marshal(v)
if err != nil {
return nil, "", err
}
return path, string(value), nil
}
func (c *FieldChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("jsonb_set_lax(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString(", true, 'delete_key')")
return nil
}
type ArrayChange struct {
path []string
value any
remove bool
}
func NewArrayChange(path []string, value any, remove bool) JsonUpdate {
return &ArrayChange{
path: path,
value: value,
remove: remove,
}
}
func (c *ArrayChange) getPathValue() ([]string, string, error) {
path, v := c.path, c.value
value, err := json.Marshal(v)
if err != nil {
return nil, "", err
}
return path, string(value), nil
}
func (c *ArrayChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error {
if c.remove {
return c.removeFromArray(builder, changes, i)
}
return c.addToArray(builder, changes, i)
}
func (c *ArrayChange) addToArray(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("zitadel.jsonb_array_append(")
// to mitigate the possibility of having duplicate values in the array
builder.WriteString("zitadel.jsonb_array_remove(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
// zitadel.jsonb_array_remove
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString("::TEXT)")
// zitadel.jsonb_array_append
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' {
builder.WriteString("::TEXT")
} else {
builder.WriteString("::NUMERIC")
}
builder.WriteString(")")
return nil
}
func (c *ArrayChange) removeFromArray(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("zitadel.jsonb_array_remove(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString("::TEXT")
builder.WriteString(")")
return nil
}
var _ JsonUpdate = (*ArrayChange)(nil)
type jsonChanges struct {
column database.Column
changes []JsonUpdate
}
var _ database.Change = (*jsonChanges)(nil)
func NewJsonChanges(col database.Column, changes ...JsonUpdate) database.Change {
return &jsonChanges{
column: col,
changes: changes,
}
}
func (c *jsonChanges) Matches(x any) bool {
toMatch, ok := x.(*jsonChanges)
if !ok {
return false
}
if !c.column.Equals(toMatch.column) {
return false
}
if len(c.changes) != len(toMatch.changes) {
return false
}
for _, c1 := range c.changes {
found := false
for _, c2 := range toMatch.changes {
if reflect.DeepEqual(c1, c2) {
found = true
}
}
if !found {
return false
}
}
return true
}
func (c *jsonChanges) String() string {
return "database.json.jsonChange"
}
func (c jsonChanges) Write(builder *database.StatementBuilder) error {
c.column.WriteUnqualified(builder)
builder.WriteString(" = ")
if c.changes == nil {
return nil
}
return c.changes[len(c.changes)-1].writeUpdate(builder, c, len(c.changes)-2)
}
func (c jsonChanges) IsOnColumn(col database.Column) bool {
return c.column.Equals(col)
}
@@ -0,0 +1,250 @@
package json
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
func TestFieldChange(t *testing.T) {
for _, test := range []struct {
name string
change database.Change
output string
}{
{
name: "single json update",
change: func() database.Change {
col := database.NewColumn("table", "column")
change := NewFieldChange([]string{"path"}, "value")
changes := NewJsonChanges(col, change)
return changes
}(),
output: `column = jsonb_set_lax(table.column, $1, $2, true, 'delete_key')`,
},
{
name: "two json update",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewFieldChange([]string{"path"}, "value")
change2 := NewFieldChange([]string{"path"}, "value")
changes := NewJsonChanges(col, change1, change2)
return changes
}(),
output: `column = jsonb_set_lax(jsonb_set_lax(table.column, $1, $2, true, 'delete_key'), $3, $4, true, 'delete_key')`,
},
} {
t.Run(test.name, func(t *testing.T) {
builder := database.StatementBuilder{}
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.output, builder.String())
})
}
}
func TestArrayChange(t *testing.T) {
for _, test := range []struct {
name string
change database.Change
output string
}{
{
name: "one json array add",
change: func() database.Change {
col := database.NewColumn("table", "column")
change := NewArrayChange([]string{"path"}, "value", false)
changes := NewJsonChanges(col, change)
return changes
}(),
output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT)`,
},
{
name: "two json array add",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", false)
change2 := NewArrayChange([]string{"path"}, "value", false)
changes := NewJsonChanges(col, change1, change2)
return changes
}(),
output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`,
},
{
name: "one json array remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change := NewArrayChange([]string{"path"}, "value", true)
changes := NewJsonChanges(col, change)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(table.column, $1, $2::TEXT)`,
},
{
name: "two json array remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", true)
change2 := NewArrayChange([]string{"path"}, "value", true)
changes := NewJsonChanges(col, change1, change2)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT)`,
},
} {
t.Run(test.name, func(t *testing.T) {
builder := database.StatementBuilder{}
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.output, builder.String())
})
}
}
func TestArrayMixedChange(t *testing.T) {
for _, test := range []struct {
name string
change database.Change
output string
}{
{
name: "one json array add, one json remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", false)
change2 := NewArrayChange([]string{"path"}, "value", true)
changes := NewJsonChanges(col, change1, change2)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT)`,
},
{
name: "one json array remove, one json array add",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", true)
change2 := NewArrayChange([]string{"path"}, "value", false)
changes := NewJsonChanges(col, change1, change2)
return changes
}(),
output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT)`,
},
{
name: "one json array add, one json array remove, one array add",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", false)
change2 := NewArrayChange([]string{"path"}, "value", true)
change3 := NewArrayChange([]string{"path)"}, "value", false)
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT), $9, $10::TEXT)`,
},
{
name: "one json array remove, one json array add, one array remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", true)
change2 := NewArrayChange([]string{"path"}, "value", false)
change3 := NewArrayChange([]string{"path)"}, "value", true)
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`,
},
} {
t.Run(test.name, func(t *testing.T) {
builder := database.StatementBuilder{}
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.output, builder.String())
})
}
}
// TODO
func TestFieldArrayMixedChange(t *testing.T) {
for _, test := range []struct {
name string
change database.Change
output string
}{
{
name: "one field change, one json array add, one json remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewFieldChange([]string{"path"}, "value")
change2 := NewArrayChange([]string{"path)"}, "value", false)
change3 := NewArrayChange([]string{"path"}, "value", true)
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(jsonb_set_lax(table.column, $1, $2, true, 'delete_key'), $3, $4::TEXT), $5, $6::TEXT), $7, $8::TEXT)`,
},
{
name: "one json array add, one field change, one json remove",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path)"}, "value", false)
change2 := NewFieldChange([]string{"path"}, "value")
change3 := NewArrayChange([]string{"path"}, "value", true)
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = zitadel.jsonb_array_remove(jsonb_set_lax(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6, true, 'delete_key'), $7, $8::TEXT)`,
},
{
name: "one json remove, one json array, add one field change",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path"}, "value", true)
change2 := NewArrayChange([]string{"path)"}, "value", false)
change3 := NewFieldChange([]string{"path"}, "value")
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = jsonb_set_lax(zitadel.jsonb_array_append(zitadel.jsonb_array_remove(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4::TEXT), $5, $6::TEXT), $7, $8, true, 'delete_key')`,
},
{
name: "one json remove, one field change, one json array add",
change: func() database.Change {
col := database.NewColumn("table", "column")
change1 := NewArrayChange([]string{"path"}, "value", true)
change2 := NewFieldChange([]string{"path"}, "value")
change3 := NewArrayChange([]string{"path)"}, "value", false)
changes := NewJsonChanges(col, change1, change2, change3)
return changes
}(),
output: `column = zitadel.jsonb_array_append(zitadel.jsonb_array_remove(jsonb_set_lax(zitadel.jsonb_array_remove(table.column, $1, $2::TEXT), $3, $4, true, 'delete_key'), $5, $6::TEXT), $7, $8::TEXT)`,
},
} {
t.Run(test.name, func(t *testing.T) {
builder := database.StatementBuilder{}
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.output, builder.String())
})
}
}
@@ -92,7 +92,10 @@ func (i *idProvider) Update(ctx context.Context, client database.QueryExecutor,
i.InstanceIDCondition(instanceID),
i.OrgIDCondition(orgID),
}
database.Changes(changes).Write(&builder)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
writeCondition(&builder, database.And(conditions...))
stmt := builder.String()
@@ -110,7 +110,10 @@ func (i instance) Update(ctx context.Context, client database.QueryExecutor, id
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.instances SET `)
database.Changes(changes).Write(&builder)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
idCondition := i.IDCondition(id)
writeCondition(&builder, idCondition)
@@ -149,15 +152,19 @@ func (i instance) SetUpdatedAt(time time.Time) database.Change {
func (i instance) SetIAMProject(id string) database.Change {
return database.NewChange(i.IAMProjectIDColumn(), id)
}
func (i instance) SetDefaultOrg(id string) database.Change {
return database.NewChange(i.DefaultOrgIDColumn(), id)
}
func (i instance) SetDefaultLanguage(lang language.Tag) database.Change {
return database.NewChange(i.DefaultLanguageColumn(), lang.String())
}
func (i instance) SetConsoleClientID(id string) database.Change {
return database.NewChange(i.ConsoleClientIDColumn(), id)
}
func (i instance) SetConsoleAppID(id string) database.Change {
return database.NewChange(i.ConsoleAppIDColumn(), id)
}
@@ -182,7 +189,6 @@ func (i instance) NameCondition(op database.TextOperation, name string) database
// ExistsDomain creates a correlated [database.Exists] condition on instance_domains.
// Use this filter to make sure the Instance returned contains a specific domain.
// of the instance in the aggregated result.
// Example usage:
//
// domainRepo := instanceRepo.Domains(true) // ensure domains are loaded/aggregated
@@ -93,7 +93,10 @@ func (i instanceDomain) Update(ctx context.Context, client database.QueryExecuto
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.instance_domains SET `)
database.Changes(changes).Write(&builder)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
writeCondition(&builder, condition)
@@ -721,7 +721,8 @@ func TestInstanceDomainChanges(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var builder database.StatementBuilder
test.change.Write(&builder)
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.expected, builder.String())
})
}
@@ -108,7 +108,10 @@ func (o org) Update(ctx context.Context, client database.QueryExecutor, conditio
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.organizations SET `)
database.Changes(changes).Write(&builder)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
writeCondition(&builder, condition)
stmt := builder.String()
@@ -107,7 +107,10 @@ func (o orgDomain) Update(ctx context.Context, client database.QueryExecutor, co
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.org_domains SET `)
database.Changes(changes).Write(&builder)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
writeCondition(&builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
@@ -928,7 +928,8 @@ func TestOrganizationDomainChanges(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var builder database.StatementBuilder
test.change.Write(&builder)
err := test.change.Write(&builder)
require.NoError(t, err)
assert.Equal(t, test.expected, builder.String())
})
}
@@ -60,7 +60,10 @@ func (p projectRole) Update(ctx context.Context, client database.QueryExecutor,
changes = append(changes, database.NewChange(p.UpdatedAtColumn(), database.NullInstruction))
}
builder := database.NewStatementBuilder(`UPDATE zitadel.project_roles SET `)
database.Changes(changes).Write(builder)
err := database.Changes(changes).Write(builder)
if err != nil {
return 0, err
}
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -98,6 +98,7 @@ var (
ProjectRoleRelationalProjection *handler.Handler
OrganizationMetadataRelationalProjection *handler.Handler
ProjectGrantRelationalProjection *handler.Handler
SettingsRelationalProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
OrgDomainVerifiedFields *handler.FieldHandler
@@ -219,6 +220,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
OrganizationRelationalProjection = newOrgRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organizations_relational"]))
InstanceDomainRelationalProjection = newInstanceDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_domains_relational"]))
OrganizationDomainRelationalProjection = newOrgDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_domains_relational"]))
SettingsRelationalProjection = newSettingsRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["settings_relational"]))
IDPTemplateRelationalProjection = newIDPTemplateRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_templates_relational"]))
ProjectRelationalProjection = newProjectRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects_relational"]))
ProjectRoleRelationalProjection = newProjectRoleRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_roles_relational"]))
@@ -413,6 +415,7 @@ func newProjectionsList() {
OrganizationRelationalProjection,
InstanceDomainRelationalProjection,
OrganizationDomainRelationalProjection,
SettingsRelationalProjection,
IDPTemplateRelationalProjection,
ProjectRelationalProjection,
ProjectRoleRelationalProjection,
File diff suppressed because it is too large Load Diff