Files
zitadel/backend/v3/storage/database/repository/project_role.go
T
Tim MöhlmannandGitHub a45908b364 feat(rt): project repository (#10789)
# Which Problems Are Solved

Add projects to the relational tables

# How the Problems Are Solved

- Define table migrations
- Define and implement Project and Project Role repositories.
- Provide projection handlers to populate the relational tables.

# Additional Changes

- Statement Builder now has a constructor which allows setting of a base
query with arguments.
- Certain operations, like Get, Update and Delete require the Primary
Key to be set as conditions. However, this requires knowledge of the
implementation and table definition. This PR proposes an additional
condition for repositories: `PrimaryKeyCondition`. This gives clarity on
the required IDs for these operations.
- Added couple of helpers to the repository package, to allow more DRY
code.
- getOne / getMany: generic functions for query execution and scanning.
- checkRestrictingColumns, checkPkCondition: simplify condition
checking, instead of using ladders of conditionals.
- Added a couple of helpers to the repository test package:
  - Transaction, savepoint and rollback helpers.
- Create instance and organization helpers for objects that depend on
them (like projects).

# Additional Context

- after https://github.com/zitadel/zitadel/pull/10809
- closes #10765
2025-10-01 09:47:04 +00:00

204 lines
6.5 KiB
Go

package repository
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
type projectRole struct{}
func (p projectRole) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.ProjectRole, error) {
builder, err := p.prepareQuery(opts)
if err != nil {
return nil, err
}
return getOne[domain.ProjectRole](ctx, client, builder)
}
func (p projectRole) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.ProjectRole, error) {
builder, err := p.prepareQuery(opts)
if err != nil {
return nil, err
}
return getMany[domain.ProjectRole](ctx, client, builder)
}
const insertProjectRoleStmt = `INSERT INTO zitadel.project_roles(
instance_id, organization_id, project_id, key, display_name, role_group
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING created_at, updated_at`
func (p projectRole) Create(ctx context.Context, client database.QueryExecutor, role *domain.ProjectRole) error {
builder := database.NewStatementBuilder(insertProjectRoleStmt,
role.InstanceID,
role.OrganizationID,
role.ProjectID,
role.Key,
role.DisplayName,
role.RoleGroup,
)
return client.QueryRow(ctx, builder.String(), builder.Args()...).
Scan(&role.CreatedAt, &role.UpdatedAt)
}
func (p projectRole) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
if len(changes) == 0 {
return 0, database.ErrNoChanges
}
if err := checkPKCondition(p, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(p.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(p.UpdatedAtColumn(), database.NullInstruction))
}
builder := database.NewStatementBuilder(`UPDATE zitadel.project_roles SET `)
database.Changes(changes).Write(builder)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
func (p projectRole) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
if err := checkPKCondition(p, condition); err != nil {
return 0, err
}
builder := database.NewStatementBuilder(`DELETE FROM zitadel.project_roles`)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
func (p projectRole) SetUpdatedAt(updatedAt time.Time) database.Change {
return database.NewChange(p.UpdatedAtColumn(), updatedAt)
}
func (p projectRole) SetDisplayName(displayName string) database.Change {
return database.NewChange(p.DisplayNameColumn(), displayName)
}
func (p projectRole) SetRoleGroup(roleGroup string) database.Change {
return database.NewChange(p.RoleGroupColumn(), roleGroup)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
func (p projectRole) PrimaryKeyCondition(instanceID, projectID, key string) database.Condition {
return database.And(
p.InstanceIDCondition(instanceID),
p.ProjectIDCondition(projectID),
p.KeyCondition(key),
)
}
func (p projectRole) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(p.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
func (p projectRole) ProjectIDCondition(projectID string) database.Condition {
return database.NewTextCondition(p.ProjectIDColumn(), database.TextOperationEqual, projectID)
}
func (p projectRole) KeyCondition(key string) database.Condition {
return database.NewTextCondition(p.KeyColumn(), database.TextOperationEqual, key)
}
func (p projectRole) DisplayNameCondition(op database.TextOperation, displayName string) database.Condition {
return database.NewTextCondition(p.DisplayNameColumn(), op, displayName)
}
func (p projectRole) RoleGroupCondition(op database.TextOperation, roleGroup string) database.Condition {
return database.NewTextCondition(p.RoleGroupColumn(), op, roleGroup)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
func (projectRole) unqualifiedTableName() string {
return "project_roles"
}
// PrimaryKeyColumns implements the [pkRepository] interface
func (p projectRole) PrimaryKeyColumns() []database.Column {
return []database.Column{
p.InstanceIDColumn(),
p.ProjectIDColumn(),
p.KeyColumn(),
}
}
func (p projectRole) InstanceIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "instance_id")
}
func (p projectRole) OrganizationIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "organization_id")
}
func (p projectRole) ProjectIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "project_id")
}
func (p projectRole) CreatedAtColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "created_at")
}
func (p projectRole) UpdatedAtColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "updated_at")
}
func (p projectRole) KeyColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "key")
}
func (p projectRole) DisplayNameColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "display_name")
}
func (p projectRole) RoleGroupColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "role_group")
}
// -------------------------------------------------------------
// helpers
// -------------------------------------------------------------
const queryProjectRoleStmt = `SELECT
project_roles.instance_id,
project_roles.organization_id,
project_roles.project_id,
project_roles.created_at,
project_roles.updated_at,
project_roles.key,
project_roles.display_name,
project_roles.role_group
FROM zitadel.project_roles`
func (p projectRole) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if err := checkRestrictingColumns(options.Condition, p.InstanceIDColumn(), p.ProjectIDColumn()); err != nil {
return nil, err
}
builder := database.NewStatementBuilder(queryProjectRoleStmt)
options.Write(builder)
return builder, nil
}