diff --git a/backend/v3/domain/instance.go b/backend/v3/domain/instance.go index 5c4782461c..49beb3805b 100644 --- a/backend/v3/domain/instance.go +++ b/backend/v3/domain/instance.go @@ -2,7 +2,6 @@ package domain import ( "context" - "database/sql" "time" "github.com/zitadel/zitadel/backend/v3/storage/cache" @@ -10,11 +9,16 @@ import ( ) type Instance struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt sql.Null[time.Time] `json:"-"` - UpdatedAt sql.Null[time.Time] `json:"-"` - DeletedAt sql.Null[time.Time] `json:"-"` + ID string `json:"id"` + Name string `json:"name"` + DefaultOrgID string `json:"default_org_id"` + IAMProjectID string `json:"iam_project_id"` + ConsoleClientId string `json:"console_client_id"` + ConsoleAppID string `json:"console_app_id"` + DefaultLanguage string `json:"default_language"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` } type instanceCacheIndex uint8 @@ -40,6 +44,16 @@ type instanceColumns interface { IDColumn() database.Column // NameColumn returns the column for the name field. NameColumn() database.Column + // DefaultOrgIdColumn returns the column for the default org id field + DefaultOrgIdColumn() database.Column + // IAMProjectIDColumn returns the column for the default IAM org id field + IAMProjectIDColumn() database.Column + // ConsoleClientIDColumn returns the column for the default IAM org id field + ConsoleClientIDColumn() database.Column + // ConsoleAppIDColumn returns the column for the console client id field + ConsoleAppIDColumn() database.Column + // DefaultLanguageColumn returns the column for the default language field + DefaultLanguageColumn() database.Column // CreatedAtColumn returns the column for the created at field. CreatedAtColumn() database.Column // UpdatedAtColumn returns the column for the updated at field. @@ -72,7 +86,7 @@ type InstanceRepository interface { // Member returns the member repository which is a sub repository of the instance repository. // Member() MemberRepository - Get(ctx context.Context, opts ...database.QueryOption) (*Instance, error) + Get(ctx context.Context, opts ...database.Condition) (*Instance, error) Create(ctx context.Context, instance *Instance) error Update(ctx context.Context, condition database.Condition, changes ...database.Change) error diff --git a/backend/v3/storage/database/condition.go b/backend/v3/storage/database/condition.go index e47b520dd1..702a8f11d2 100644 --- a/backend/v3/storage/database/condition.go +++ b/backend/v3/storage/database/condition.go @@ -12,6 +12,7 @@ type and struct { // Write implements [Condition]. func (a *and) Write(builder *StatementBuilder) { + builder.WriteString(" WHERE ") if len(a.conditions) > 1 { builder.WriteString("(") defer builder.WriteString(")") diff --git a/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql index aaac796922..6cd4a89124 100644 --- a/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql +++ b/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql @@ -10,4 +10,3 @@ CREATE TABLE IF NOT EXISTS zitadel.instances( updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ DEFAULT NULL ); --- CREATE UNIQUE INDEX instance_name_index ON zitadel.instances (name); diff --git a/backend/v3/storage/database/events_testing/instance_test.go b/backend/v3/storage/database/events_testing/instance_test.go index 8c0bf04709..d655cbc2d1 100644 --- a/backend/v3/storage/database/events_testing/instance_test.go +++ b/backend/v3/storage/database/events_testing/instance_test.go @@ -54,7 +54,7 @@ func TestMain(m *testing.M) { func TestServer_TestInstanceAddReduces(t *testing.T) { instanceName := gofakeit.Name() - beforeAdd := time.Now() + beforeCreate := time.Now() _, err := SystemClient.CreateInstance(CTX, &system.CreateInstanceRequest{ InstanceName: instanceName, Owner: &system.CreateInstanceRequest_Machine_{ @@ -65,7 +65,7 @@ func TestServer_TestInstanceAddReduces(t *testing.T) { }, }, }) - afterAdd := time.Now() + afterCreate := time.Now() require.NoError(t, err) @@ -73,9 +73,7 @@ func TestServer_TestInstanceAddReduces(t *testing.T) { retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { instance, err := instanceRepo.Get(CTX, - database.WithCondition( - instanceRepo.NameCondition(database.TextOperationEqual, instanceName), - ), + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) require.NoError(ttt, err) // event instance.added @@ -89,9 +87,9 @@ func TestServer_TestInstanceAddReduces(t *testing.T) { // event instance.default.language.set require.NotNil(t, instance.DefaultLanguage) // event instance.added - assert.WithinRange(t, instance.CreatedAt, beforeAdd, afterAdd) + assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate) // event instance.added - assert.WithinRange(t, instance.UpdatedAt, beforeAdd, afterAdd) + assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate) require.Nil(t, instance.DeletedAt) }, retryDuration, tick) } @@ -121,9 +119,7 @@ func TestServer_TestInstanceUpdateNameReduces(t *testing.T) { retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { instance, err := instanceRepo.Get(CTX, - database.WithCondition( - instanceRepo.NameCondition(database.TextOperationEqual, instanceName), - ), + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) require.NoError(ttt, err) // event instance.changed @@ -145,23 +141,19 @@ func TestServer_TestInstanceDeleteReduces(t *testing.T) { }) require.NoError(t, err) - beforeDelete := time.Now() _, err = SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{ InstanceId: res.InstanceId, }) require.NoError(t, err) - afterDelete := time.Now() instanceRepo := repository.InstanceRepository(pool) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) assert.EventuallyWithT(t, func(ttt *assert.CollectT) { instance, err := instanceRepo.Get(CTX, - database.WithCondition( - instanceRepo.NameCondition(database.TextOperationEqual, instanceName), - ), + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) // event instance.removed - assert.WithinRange(t, *instance.DeletedAt, beforeDelete, afterDelete) + require.Nil(t, instance) require.NoError(ttt, err) }, retryDuration, tick) } diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go index 4f4369f38c..4e01aae6dd 100644 --- a/backend/v3/storage/database/repository/instance.go +++ b/backend/v3/storage/database/repository/instance.go @@ -3,6 +3,7 @@ package repository import ( "context" "errors" + "time" "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" @@ -26,34 +27,32 @@ func InstanceRepository(client database.QueryExecutor) domain.InstanceRepository // repository // ------------------------------------------------------------- -const queryInstanceStmt = `SELECT id, name, created_at, updated_at, deleted_at` + +const queryInstanceStmt = `SELECT id, name, default_org_id, iam_project_id, console_client_id, console_app_id, default_language, created_at, updated_at, deleted_at` + ` FROM zitadel.instances` // Get implements [domain.InstanceRepository]. -func (i *instance) Get(ctx context.Context, opts ...database.QueryOption) (*domain.Instance, error) { +// func (i *instance) Get(ctx context.Context, opts ...database.QueryOption) (*domain.Instance, error) { +func (i *instance) Get(ctx context.Context, opts ...database.Condition) (*domain.Instance, error) { i.builder = database.StatementBuilder{} - options := new(database.QueryOpts) - for _, opt := range opts { - opt(options) - } i.builder.WriteString(queryInstanceStmt) - options.WriteCondition(&i.builder) - options.WriteOrderBy(&i.builder) - options.WriteLimit(&i.builder) - options.WriteOffset(&i.builder) + + isNotDeletedCondition := database.IsNull(i.DeletedAtColumn()) + opts = append(opts, isNotDeletedCondition) + andCondition := database.And(opts...) + andCondition.Write(&i.builder) return scanInstance(i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...)) } -const createInstanceStmt = `INSERT INTO zitadel.instances (id, name)` + - ` VALUES ($1, $2)` + +const createInstanceStmt = `INSERT INTO zitadel.instances (id, name, default_org_id, iam_project_id, console_client_id, console_app_id, default_language)` + + ` VALUES ($1, $2, $3, $4, $5, $6, $7)` + ` RETURNING created_at, updated_at` // Create implements [domain.InstanceRepository]. func (i *instance) Create(ctx context.Context, instance *domain.Instance) error { i.builder = database.StatementBuilder{} - i.builder.AppendArgs(instance.ID, instance.Name) + i.builder.AppendArgs(instance.ID, instance.Name, instance.DefaultOrgID, instance.IAMProjectID, instance.ConsoleClientId, instance.ConsoleAppID, instance.DefaultLanguage) i.builder.WriteString(createInstanceStmt) return i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...).Scan(&instance.CreatedAt, &instance.UpdatedAt) @@ -62,7 +61,7 @@ func (i *instance) Create(ctx context.Context, instance *domain.Instance) error // Update implements [domain.InstanceRepository]. func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error { i.builder = database.StatementBuilder{} - i.builder.WriteString(`UPDATE human_users SET `) + i.builder.WriteString(`UPDATE zitadel.instances SET `) database.Changes(changes).Write(&i.builder) i.writeCondition(condition) @@ -73,11 +72,13 @@ func (i instance) Update(ctx context.Context, condition database.Condition, chan // Delete implements [domain.InstanceRepository]. func (i instance) Delete(ctx context.Context, condition database.Condition) error { - i.builder.WriteString("DELETE FROM instance") - if condition == nil { return errors.New("Delete must contain a condition") // (otherwise ALL instances will be deleted) } + i.builder = database.StatementBuilder{} + i.builder.WriteString(`UPDATE zitadel.instances SET deleted_at = $1`) + i.builder.AppendArgs(time.Now()) + i.writeCondition(condition) return i.client.Exec(ctx, i.builder.String(), i.builder.Args()...) } @@ -124,6 +125,31 @@ func (instance) CreatedAtColumn() database.Column { return database.NewColumn("created_at") } +// DefaultOrgIdColumn implements [domain.instanceColumns]. +func (instance) DefaultOrgIdColumn() database.Column { + return database.NewColumn("default_org_id") +} + +// IAMProjectIDColumn implements [domain.instanceColumns]. +func (instance) IAMProjectIDColumn() database.Column { + return database.NewColumn("iam_project_id") +} + +// ConsoleClientIDColumn implements [domain.instanceColumns]. +func (instance) ConsoleClientIDColumn() database.Column { + return database.NewColumn("console_client_id") +} + +// ConsoleAppIDColumn implements [domain.instanceColumns]. +func (instance) ConsoleAppIDColumn() database.Column { + return database.NewColumn("console_app_id") +} + +// DefaultLanguageColumn implements [domain.instanceColumns]. +func (instance) DefaultLanguageColumn() database.Column { + return database.NewColumn("default_language") +} + // UpdatedAtColumn implements [domain.instanceColumns]. func (instance) UpdatedAtColumn() database.Column { return database.NewColumn("updated_at") @@ -147,11 +173,22 @@ func scanInstance(scanner database.Scanner) (*domain.Instance, error) { err := scanner.Scan( &instance.ID, &instance.Name, + &instance.DefaultOrgID, + &instance.IAMProjectID, + &instance.ConsoleClientId, + &instance.ConsoleAppID, + &instance.DefaultLanguage, &instance.CreatedAt, &instance.UpdatedAt, &instance.DeletedAt, ) if err != nil { + // if no results returned, this is not a error + // it just means the instance was not found + // the caller should check if the returned instance is nil + if err.Error() == "no rows in result set" { + return nil, nil + } return nil, err } diff --git a/backend/v3/storage/database/repository/instance_test.go b/backend/v3/storage/database/repository/instance_test.go index 4fac9335ec..64c2ff20ca 100644 --- a/backend/v3/storage/database/repository/instance_test.go +++ b/backend/v3/storage/database/repository/instance_test.go @@ -2,100 +2,130 @@ package repository_test import ( "context" - "fmt" "testing" + "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" - "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock" "github.com/zitadel/zitadel/backend/v3/storage/database/repository" - - "go.uber.org/mock/gomock" ) func TestCreateInstance(t *testing.T) { instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() ctx := context.Background() inst := domain.Instance{ - ID: "id", - Name: "name", + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientId: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", } + + beforeCreate := time.Now() err := instanceRepo.Create(ctx, &inst) - fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> err = %+v\n", err) - fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> inst = %+v\n", inst) require.NoError(t, err) + afterCreate := time.Now() - instt, err := instanceRepo.Get(ctx, - database.WithCondition( - instanceRepo.NameCondition(database.TextOperationEqual, "name"), - ), + instance, err := instanceRepo.Get(ctx, + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) - // require.NoError(t, err) - fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> err = %+v\n", err) - fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> inst = %+v\n", instt) - - t.Skip("tests are meant as examples and are not real tests") - t.Run("User filters", func(t *testing.T) { - client := dbmock.NewMockClient(gomock.NewController(t)) - - user := repository.UserRepository(client) - u, err := user.Get(context.Background(), - database.WithCondition( - database.And( - database.Or( - user.IDCondition("test"), - user.IDCondition("2"), - ), - user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"), - ), - ), - database.WithOrderBy(user.CreatedAtColumn()), - ) - - assert.NoError(t, err) - assert.NotNil(t, u) - }) - - t.Run("machine and human filters", func(t *testing.T) { - client := dbmock.NewMockClient(gomock.NewController(t)) - - user := repository.UserRepository(client) - machine := user.Machine() - human := user.Human() - email, err := human.GetEmail(context.Background(), database.And( - user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"), - database.Or( - machine.DescriptionCondition(database.TextOperationStartsWithIgnoreCase, "test"), - human.EmailVerifiedCondition(true), - database.IsNotNull(machine.DescriptionColumn()), - ), - )) - - assert.NoError(t, err) - assert.NotNil(t, email) - }) + require.Equal(t, inst.ID, instance.ID) + require.Equal(t, inst.Name, instance.Name) + require.Equal(t, inst.DefaultOrgID, instance.DefaultOrgID) + require.Equal(t, inst.IAMProjectID, instance.IAMProjectID) + require.Equal(t, inst.ConsoleClientId, instance.ConsoleClientId) + require.Equal(t, inst.ConsoleAppID, instance.ConsoleAppID) + require.Equal(t, inst.DefaultLanguage, instance.DefaultLanguage) + assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate) + assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate) + require.Nil(t, instance.DeletedAt) + require.NoError(t, err) } -// type dbInstruction string +func TestUpdateNameInstance(t *testing.T) { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() -// func TestArg(t *testing.T) { -// var bla any = "asdf" -// instr, ok := bla.(dbInstruction) -// assert.False(t, ok) -// assert.Empty(t, instr) -// bla = dbInstruction("asdf") -// instr, ok = bla.(dbInstruction) -// assert.True(t, ok) -// assert.Equal(t, instr, dbInstruction("asdf")) -// } + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientId: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } -// func TestWriteUser(t *testing.T) { -// t.Skip("tests are meant as examples and are not real tests") -// t.Run("update user", func(t *testing.T) { -// user := repository.UserRepository(nil) -// user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test")) -// }) -// } + err := instanceRepo.Create(ctx, &inst) + require.NoError(t, err) + + _, err = instanceRepo.Get(ctx, + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), + ) + require.NoError(t, err) + + // update name + err = instanceRepo.Update(ctx, + database.Condition( + instanceRepo.IDCondition(instanceId), + ), + instanceRepo.SetName("new_name"), + ) + require.NoError(t, err) + + instance, err := instanceRepo.Get(ctx, + instanceRepo.IDCondition(instanceId), + ) + require.NoError(t, err) + require.Equal(t, "new_name", instance.Name) +} + +func TestUpdeDeleteInstance(t *testing.T) { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() + + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientId: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + + err := instanceRepo.Create(ctx, &inst) + require.NoError(t, err) + + instance, err := instanceRepo.Get(ctx, + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), + ) + require.NotNil(t, instance) + require.NoError(t, err) + + // delete instance + err = instanceRepo.Delete(ctx, + database.Condition( + instanceRepo.IDCondition(instanceId), + ), + ) + require.NoError(t, err) + + instance, err = instanceRepo.Get(ctx, + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), + ) + require.NoError(t, err) + require.Nil(t, instance) +} diff --git a/internal/query/projection/instance_relational.go b/internal/query/projection/instance_relational.go index 8e2d45521c..d85c7a1db3 100644 --- a/internal/query/projection/instance_relational.go +++ b/internal/query/projection/instance_relational.go @@ -27,14 +27,14 @@ func (*instanceRelationalProjection) Init() *old_handler.Check { handler.NewTable([]*handler.InitColumn{ handler.NewColumn(InstanceColumnID, handler.ColumnTypeText), handler.NewColumn(InstanceColumnName, handler.ColumnTypeText, handler.Default("")), - handler.NewColumn(InstanceColumnChangeDate, handler.ColumnTypeTimestamp), - handler.NewColumn(InstanceColumnCreationDate, handler.ColumnTypeTimestamp), handler.NewColumn(InstanceColumnDefaultOrgID, handler.ColumnTypeText, handler.Default("")), handler.NewColumn(InstanceColumnProjectID, handler.ColumnTypeText, handler.Default("")), handler.NewColumn(InstanceColumnConsoleID, handler.ColumnTypeText, handler.Default("")), handler.NewColumn(InstanceColumnConsoleAppID, handler.ColumnTypeText, handler.Default("")), - handler.NewColumn(InstanceColumnSequence, handler.ColumnTypeInt64), handler.NewColumn(InstanceColumnDefaultLanguage, handler.ColumnTypeText, handler.Default("")), + handler.NewColumn(CreatedAt, handler.ColumnTypeTimestamp), + handler.NewColumn(UpdatedAt, handler.ColumnTypeTimestamp), + handler.NewColumn(DeletedAt, handler.ColumnTypeTimestamp), }, handler.NewPrimaryKey(InstanceColumnID), ), @@ -113,7 +113,7 @@ func (p *instanceRelationalProjection) reduceInstanceChanged(event eventstore.Ev } func (p *instanceRelationalProjection) reduceInstanceDelete(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.InstanceChangedEvent) + e, ok := event.(*instance.InstanceRemovedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-so2am1", "reduce.wrong.event.type %s", instance.InstanceChangedEventType) }