mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 10:27:33 +00:00
feat(db): adding org table to relational model (#10066)
# Which Problems Are Solved As an outcome of [this issue](https://github.com/zitadel/zitadel/issues/9599) we want to implement relational tables in Zitadel. For that we use new tables as a successor of the current tables used by Zitadel in `projections`, `auth` and `admin` schemas. The new logic is based on [this proposal](https://github.com/zitadel/zitadel/pull/9870). This issue does not contain the switch from CQRS to the new tables. This is change will be implemented in a later stage. We focus on the most critical tables which is user authentication. We need a table to manage organizations. ### organization fields The following fields must be managed in this table: - `id` - `instance_id` - `name` - `state` enum (active, inactive) - `created_at` - `updated_at` - `deleted_at` DISCUSS: should we add a `primary_domain` to this table so that we do not have to join on domains to return a simple org? We must ensure the unique constraints for this table matches the current commands. ### organization repository The repository must provide the following functions: Manipulations: - create - `instance_id` - `name` - update - `name` - delete Queries: - get returns single organization matching the criteria and pagination, should return error if multiple were found - list returns list of organizations matching the criteria, pagination Criteria are the following: - by id - by name pagination: - by created_at - by updated_at - by name ### organization events The following events must be applied on the table using a projection (`internal/query/projection`) - `org.added` results in create - `org.changed` sets the `name` field - `org.deactivated` sets the `state` field - `org.reactivated` sets the `state` field - `org.removed` sets the `deleted_at` field - if answer is yes to discussion: `org.domain.primary.set` sets the `primary_domain` field - `instance.removed` sets the the `deleted_at` field if not already set ### acceptance criteria - [x] migration is implemented and gets executed - [x] domain interfaces are implemented and documented for service layer - [x] repository is implemented and implements domain interface - [x] testing - [x] the repository methods - [x] events get reduced correctly - [x] unique constraints # Additional Context Replace this example with links to related issues, discussions, discord threads, or other sources with more context. Use the Closing #issue syntax for issues that are resolved with this PR. - Closes #https://github.com/zitadel/zitadel/issues/9936 --------- Co-authored-by: adlerhurst <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
@@ -74,11 +75,61 @@ func TestCreateInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
// change the name to make sure same only the id clashes
|
||||
inst.Name = gofakeit.Name()
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
err: errors.New("instance id already exists"),
|
||||
},
|
||||
func() struct {
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
instance domain.Instance
|
||||
err error
|
||||
} {
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
return struct {
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
instance domain.Instance
|
||||
err error
|
||||
}{
|
||||
name: "adding instance with same name twice",
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
require.NoError(t, err)
|
||||
|
||||
// change the id
|
||||
inst.ID = instanceId
|
||||
return &inst
|
||||
},
|
||||
instance: domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
},
|
||||
// two instances can have the sane name
|
||||
err: nil,
|
||||
}
|
||||
}(),
|
||||
{
|
||||
name: "adding instance with no id",
|
||||
instance: func() domain.Instance {
|
||||
@@ -113,7 +164,7 @@ func TestCreateInstance(t *testing.T) {
|
||||
// create instance
|
||||
beforeCreate := time.Now()
|
||||
err := instanceRepo.Create(ctx, instance)
|
||||
require.Equal(t, tt.err, err)
|
||||
assert.Equal(t, tt.err, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -121,20 +172,20 @@ func TestCreateInstance(t *testing.T) {
|
||||
|
||||
// check instance values
|
||||
instance, err = instanceRepo.Get(ctx,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instance.Name),
|
||||
instance.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.instance.ID, instance.ID)
|
||||
require.Equal(t, tt.instance.Name, instance.Name)
|
||||
require.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
|
||||
require.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
|
||||
require.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
|
||||
require.Nil(t, instance.DeletedAt)
|
||||
assert.Equal(t, tt.instance.ID, instance.ID)
|
||||
assert.Equal(t, tt.instance.Name, instance.Name)
|
||||
assert.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
|
||||
assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
|
||||
assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
|
||||
assert.Nil(t, instance.DeletedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -144,6 +195,7 @@ func TestUpdateInstance(t *testing.T) {
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
rowsAffected int64
|
||||
getErr error
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
@@ -191,10 +243,11 @@ func TestUpdateInstance(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete instance
|
||||
err = instanceRepo.Delete(ctx,
|
||||
instanceRepo.IDCondition(inst.ID),
|
||||
affectedRows, err := instanceRepo.Delete(ctx,
|
||||
inst.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), affectedRows)
|
||||
|
||||
return &inst
|
||||
},
|
||||
@@ -211,6 +264,7 @@ func TestUpdateInstance(t *testing.T) {
|
||||
return &inst
|
||||
},
|
||||
rowsAffected: 0,
|
||||
getErr: repository.ErrResourceDoesNotExist,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -224,13 +278,13 @@ func TestUpdateInstance(t *testing.T) {
|
||||
// update name
|
||||
newName := "new_" + instance.Name
|
||||
rowsAffected, err := instanceRepo.Update(ctx,
|
||||
instanceRepo.IDCondition(instance.ID),
|
||||
instance.ID,
|
||||
instanceRepo.SetName(newName),
|
||||
)
|
||||
afterUpdate := time.Now()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.rowsAffected, rowsAffected)
|
||||
assert.Equal(t, tt.rowsAffected, rowsAffected)
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return
|
||||
@@ -238,13 +292,13 @@ func TestUpdateInstance(t *testing.T) {
|
||||
|
||||
// check instance values
|
||||
instance, err = instanceRepo.Get(ctx,
|
||||
instanceRepo.IDCondition(instance.ID),
|
||||
instance.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.getErr, err)
|
||||
|
||||
require.Equal(t, newName, instance.Name)
|
||||
require.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
|
||||
require.Nil(t, instance.DeletedAt)
|
||||
assert.Equal(t, newName, instance.Name)
|
||||
assert.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
|
||||
assert.Nil(t, instance.DeletedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -252,9 +306,9 @@ func TestUpdateInstance(t *testing.T) {
|
||||
func TestGetInstance(t *testing.T) {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
conditionClauses []database.Condition
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
err error
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
@@ -280,45 +334,17 @@ func TestGetInstance(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.IDCondition(instanceId)},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path get using name",
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, instanceName)},
|
||||
}
|
||||
}(),
|
||||
{
|
||||
name: "get non existent instance",
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
_ = domain.Instance{
|
||||
ID: instanceId,
|
||||
inst := domain.Instance{
|
||||
ID: "get non existent instance",
|
||||
}
|
||||
return nil
|
||||
return &inst
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, "non-existent-instance-name")},
|
||||
err: repository.ErrResourceDoesNotExist,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -333,22 +359,25 @@ func TestGetInstance(t *testing.T) {
|
||||
|
||||
// check instance values
|
||||
returnedInstance, err := instanceRepo.Get(ctx,
|
||||
tt.conditionClauses...,
|
||||
instance.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
if instance == nil {
|
||||
require.Nil(t, instance, returnedInstance)
|
||||
if tt.err != nil {
|
||||
require.Equal(t, tt.err, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, returnedInstance.ID, instance.ID)
|
||||
require.Equal(t, returnedInstance.Name, instance.Name)
|
||||
require.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
|
||||
if instance.ID == "get non existent instance" {
|
||||
assert.Nil(t, returnedInstance)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, returnedInstance.ID, instance.ID)
|
||||
assert.Equal(t, returnedInstance.Name, instance.Name)
|
||||
assert.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -504,19 +533,19 @@ func TestListInstance(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
if tt.noInstanceReturned {
|
||||
require.Nil(t, returnedInstances)
|
||||
assert.Nil(t, returnedInstances)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, len(instances), len(returnedInstances))
|
||||
assert.Equal(t, len(instances), len(returnedInstances))
|
||||
for i, instance := range instances {
|
||||
require.Equal(t, returnedInstances[i].ID, instance.ID)
|
||||
require.Equal(t, returnedInstances[i].Name, instance.Name)
|
||||
require.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
|
||||
assert.Equal(t, returnedInstances[i].ID, instance.ID)
|
||||
assert.Equal(t, returnedInstances[i].Name, instance.Name)
|
||||
assert.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -524,18 +553,19 @@ func TestListInstance(t *testing.T) {
|
||||
|
||||
func TestDeleteInstance(t *testing.T) {
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T)
|
||||
conditionClauses database.Condition
|
||||
name string
|
||||
testFunc func(ctx context.Context, t *testing.T)
|
||||
instanceID string
|
||||
noOfDeletedRows int64
|
||||
}
|
||||
tests := []test{
|
||||
func() test {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceId := gofakeit.Name()
|
||||
var noOfInstances int64 = 1
|
||||
return test{
|
||||
name: "happy path delete single instance filter id",
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
@@ -556,75 +586,15 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instances[i] = &inst
|
||||
}
|
||||
},
|
||||
conditionClauses: instanceRepo.IDCondition(instanceId),
|
||||
instanceID: instanceId,
|
||||
noOfDeletedRows: noOfInstances,
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path delete single instance filter name",
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
},
|
||||
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
non_existent_instance_name := gofakeit.Name()
|
||||
return test{
|
||||
name: "delete non existent instance",
|
||||
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, non_existent_instance_name),
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "multiple instance filter on name",
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 5
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
},
|
||||
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
name: "delete non existent instance",
|
||||
instanceID: non_existent_instance_name,
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
@@ -655,12 +625,15 @@ func TestDeleteInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
// delete instance
|
||||
err := instanceRepo.Delete(ctx,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
affectedRows, err := instanceRepo.Delete(ctx,
|
||||
instances[0].ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), affectedRows)
|
||||
},
|
||||
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
instanceID: instanceName,
|
||||
// this test should return 0 affected rows as the instance was already deleted
|
||||
noOfDeletedRows: 0,
|
||||
}
|
||||
}(),
|
||||
}
|
||||
@@ -674,17 +647,18 @@ func TestDeleteInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
// delete instance
|
||||
err := instanceRepo.Delete(ctx,
|
||||
tt.conditionClauses,
|
||||
noOfDeletedRows, err := instanceRepo.Delete(ctx,
|
||||
tt.instanceID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, noOfDeletedRows, tt.noOfDeletedRows)
|
||||
|
||||
// check instance was deleted
|
||||
instance, err := instanceRepo.Get(ctx,
|
||||
tt.conditionClauses,
|
||||
tt.instanceID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, instance)
|
||||
require.Equal(t, err, repository.ErrResourceDoesNotExist)
|
||||
assert.Nil(t, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user