Files
zitadel/backend/v3/domain/organization.go

300 lines
7.8 KiB
Go
Raw Normal View History

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>
2025-07-14 21:27:14 +02:00
package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type OrgState string
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>
2025-07-14 21:27:14 +02:00
const (
OrgStateActive OrgState = "active"
OrgStateInactive OrgState = "inactive"
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>
2025-07-14 21:27:14 +02:00
)
type Organization struct {
ID string `json:"id,omitempty" db:"id"`
Name string `json:"name,omitempty" db:"name"`
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
State OrgState `json:"state,omitempty" db:"state"`
2025-07-22 19:09:56 +02:00
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
Domains []*OrganizationDomain `json:"domains,omitempty" db:"-"`
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>
2025-07-14 21:27:14 +02:00
}
2025-07-29 17:59:02 +02:00
var _ Commander = (*CreateOrganizationCommand)(nil)
type CreateOrganizationCommand struct {
InstanceID string `json:"instanceId"`
// ID is optional, if not set a new ID will be generated.
// It can be set using the [WithOrganizationID] option in [NewCreateOrganizationCommand].
ID string `json:"id,omitempty"`
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>
2025-07-14 21:27:14 +02:00
Name string `json:"name"`
2025-07-29 17:59:02 +02:00
// CreatedAt MUST NOT be set by the caller.
CreatedAt time.Time `json:"createdAt,omitzero"`
// Admins represent the commands to create the administrators.
// The Commanders MUST either be [AddOrgMemberCommand] or [CreateOrgMemberCommand].
Admins []Commander `json:"admins,omitempty"`
}
type CreateOrganizationCommandOpts interface {
applyOnCreateOrganizationCommand(cmd *CreateOrganizationCommand)
}
func NewCreateOrganizationCommand(instanceID, name string, opts ...CreateOrganizationCommandOpts) *CreateOrganizationCommand {
cmd := &CreateOrganizationCommand{
InstanceID: instanceID,
Name: name,
}
for _, opt := range opts {
opt.applyOnCreateOrganizationCommand(cmd)
}
return cmd
}
// Execute implements [Commander].
//
// DISCUSS(adlerhurst): As we need to do validation to make sure a command contains all the data required
// we can consider the following options:
// 1. Validate the command before executing it, which is what we do here.
// 2. Create an invoker which checks if the struct has a `Validate() error` method and call it in the chain of invokers.
// While the the first one is more straightforward it bloats the execute method with validation logic.
// The second one would allow us to keep the execute method clean, but could be more error prone if the method gets missed during implementation.
func (cmd *CreateOrganizationCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
if cmd.ID == "" {
cmd.ID, err = generateID()
if err != nil {
return err
}
}
close, err := opts.EnsureTx(ctx)
if err != nil {
return err
}
defer func() { err = close(ctx, err) }()
err = orgRepo(opts.DB).Create(ctx, &Organization{
ID: cmd.ID,
Name: cmd.Name,
InstanceID: cmd.InstanceID,
State: OrgStateActive,
})
if err != nil {
return err
}
for _, admin := range cmd.Admins {
if err = opts.Invoke(ctx, admin); err != nil {
return err
}
}
return nil
}
// String implements [Commander].
func (CreateOrganizationCommand) String() string {
return "CreateOrganizationCommand"
}
var (
_ Commander = (*ActivateOrganizationCommand)(nil)
)
type ActivateOrganizationCommand struct {
InstanceID string `json:"instanceId"`
OrgID string `json:"orgId"`
// UpdatedAt MUST NOT be set by the caller.
UpdatedAt time.Time `json:"updatedAt,omitzero"`
}
func NewActivateOrganizationCommand(instanceID, orgID string) *ActivateOrganizationCommand {
return &ActivateOrganizationCommand{
InstanceID: instanceID,
OrgID: orgID,
}
}
// Execute implements [Commander].
func (cmd *ActivateOrganizationCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
repo := orgRepo(opts.DB)
_, err = repo.Update(ctx,
repo.IDCondition(cmd.OrgID),
cmd.InstanceID,
repo.SetState(OrgStateActive),
)
return err
}
// String implements [Commander].
func (ActivateOrganizationCommand) String() string {
return "ActivateOrganizationCommand"
}
var (
_ Commander = (*DeactivateOrganizationCommand)(nil)
)
type DeactivateOrganizationCommand struct {
InstanceID string `json:"instanceId"`
OrgID string `json:"orgId"`
// UpdatedAt MUST NOT be set by the caller.
UpdatedAt time.Time `json:"updatedAt,omitzero"`
}
func NewDeactivateOrganizationCommand(instanceID, orgID string) *DeactivateOrganizationCommand {
return &DeactivateOrganizationCommand{
InstanceID: instanceID,
OrgID: orgID,
}
}
// Execute implements [Commander].
func (cmd *DeactivateOrganizationCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
repo := orgRepo(opts.DB)
_, err = repo.Update(ctx,
repo.IDCondition(cmd.OrgID),
cmd.InstanceID,
repo.SetState(OrgStateInactive),
)
return err
}
// String implements [Commander].
func (DeactivateOrganizationCommand) String() string {
return "DeactivateOrganizationCommand"
}
var (
_ Commander = (*DeleteOrganizationCommand)(nil)
)
type DeleteOrganizationCommand struct {
InstanceID string `json:"instanceId"`
OrgID string `json:"orgId"`
}
func NewDeleteOrganizationCommand(instanceID, orgID string) *DeleteOrganizationCommand {
return &DeleteOrganizationCommand{
InstanceID: instanceID,
OrgID: orgID,
}
}
// Execute implements [Commander].
func (cmd *DeleteOrganizationCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
repo := orgRepo(opts.DB)
_, err = repo.Delete(ctx,
repo.IDCondition(cmd.OrgID),
cmd.InstanceID,
)
return err
}
// String implements [Commander].
func (DeleteOrganizationCommand) String() string {
return "DeleteOrganizationCommand"
}
var _ Commander = (*UpdateOrganizationCommand)(nil)
type UpdateOrganizationCommand struct {
InstanceID string `json:"instanceId"`
OrgID string `json:"orgId"`
repo OrganizationRepository
changes database.Changes
opts []UpdateOrganizationCommandOpts
}
func NewUpdateOrganizationCommand(instanceID, orgID string, opts ...UpdateOrganizationCommandOpts) *UpdateOrganizationCommand {
return &UpdateOrganizationCommand{
InstanceID: instanceID,
OrgID: orgID,
opts: opts,
}
}
type UpdateOrganizationCommandOpts interface {
applyOnUpdateOrganizationCommand(cmd *UpdateOrganizationCommand)
}
// Execute implements [Commander].
func (cmd *UpdateOrganizationCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
cmd.repo = orgRepo(opts.DB)
for _, opt := range cmd.opts {
opt.applyOnUpdateOrganizationCommand(cmd)
}
if len(cmd.changes) == 0 {
return nil // No update needed if no changes are provided.
}
_, err = cmd.repo.Update(ctx,
cmd.repo.IDCondition(cmd.OrgID),
cmd.InstanceID,
2025-07-29 18:10:35 +02:00
cmd.changes...,
2025-07-29 17:59:02 +02:00
)
return err
}
// String implements [Commander].
func (UpdateOrganizationCommand) String() string {
return "UpdateOrganizationCommand"
}
type OrgsQueryOpts interface {
applyOnOrgsQuery(query *OrgsQuery)
}
var _ Commander = (*OrgsQuery)(nil)
type OrgsQuery struct {
InstanceID string
opts []OrgsQueryOpts
repo OrganizationRepository
domainRepo OrganizationDomainRepository
conditions []database.Condition
pagination Pagination
Result []*Organization
}
func NewOrgsQuery(instanceID string, opts ...OrgsQueryOpts) *OrgsQuery {
return &OrgsQuery{
InstanceID: instanceID,
opts: opts,
}
}
// Execute implements [Commander].
func (q *OrgsQuery) Execute(ctx context.Context, opts *CommandOpts) (err error) {
q.repo = orgRepo(opts.DB)
q.domainRepo = q.repo.Domains(true)
q.conditions = append(q.conditions, q.repo.InstanceIDCondition(q.InstanceID))
for _, opt := range q.opts {
opt.applyOnOrgsQuery(q)
}
q.Result, err = q.repo.List(ctx,
database.WithCondition(database.And(q.conditions...)),
database.WithLimit(q.pagination.Limit),
database.WithOffset(q.pagination.Offset),
database.WithOrderBy(!q.pagination.Ascending, q.pagination.OrderColumns...),
)
return err
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>
2025-07-14 21:27:14 +02:00
}
2025-07-29 17:59:02 +02:00
// String implements [Commander].
func (OrgsQuery) String() string {
return "OrgsQuery"
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>
2025-07-14 21:27:14 +02:00
}