Files
zitadel/backend/v3/storage/database/repository/repository.go
T
9274e008fe feat(rt): project grants (#10951)
This pull request introduces a new feature that allows adding, updating,
and querying project grants The changes are primarily in the backend and
include new database tables, repositories, and domain logic to support
project grants.

## Changes

* New `project_grants` table: A new table `zitadel.project_grants` is
introduced to store project grants It includes columns for
`instance_id`, `id`, `state`, `project_id`, `granted_org_id` and
`granting_org_id`.
* New `ProjectGrantRepository`: A new repository
`ProjectGrantRepository` is created to handle all database operations
for project grants. It provides methods to `Get`, `List`, `Create`,
`Update`, and `Delete`.
* New `project_grant_roles` table: A new table
`zitadel.project_grant_roles` is introduced to store project grant roles
It includes columns for `instance_id`, `grant_id`, `key`, `project_id`
and `project_org_id`.
* New `ProjectGrantRoleRepository`: A new repository
`ProjectGrantRoleRepository` is created to handle all database
operations for project grant roles. It provides methods to `Get`,
`List`, `Add`, and `Delete`.
* New `project_grant_relational_projection`: A new projection
`project_grant_relational_projection` is added to update the
`zitadel.project_grants` and `zitadel.project_grant_roles` tables based
on events. It handles all project grant events.
* Updated `ProjectRepository`: The `ProjectRepository` is updated to
list granted projects.

# Additional Context

Closes https://github.com/zitadel/zitadel/issues/10766

---------

Co-authored-by: Stefan Benz <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Silvan <[email protected]>
2025-11-14 11:04:16 +00:00

111 lines
3.1 KiB
Go

package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
func writeCondition(
builder *database.StatementBuilder,
condition database.Condition,
) {
if condition == nil {
return
}
builder.WriteString(" WHERE ")
condition.Write(builder)
}
func checkRestrictingColumns(
condition database.Condition,
requiredColumns ...database.Column,
) error {
for _, col := range requiredColumns {
if !condition.IsRestrictingColumn(col) {
return database.NewMissingConditionError(col)
}
}
return nil
}
// checkPKCondition checks if the Primary Key columns are part of the condition.
// This can ensure only a single row is affected by updates and deletes.
func checkPKCondition(
repo domain.Repository,
condition database.Condition,
) error {
return checkRestrictingColumns(
condition,
repo.PrimaryKeyColumns()...,
)
}
func getOne[Target any](ctx context.Context, querier database.Querier, builder *database.StatementBuilder) (*Target, error) {
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
if err != nil {
return nil, err
}
var target Target
if err := rows.(database.CollectableRows).CollectExactlyOneRow(&target); err != nil {
return nil, err
}
return &target, nil
}
func getMany[Target any](ctx context.Context, querier database.Querier, builder *database.StatementBuilder) ([]*Target, error) {
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
if err != nil {
return nil, err
}
var targets []*Target
if err := rows.(database.CollectableRows).Collect(&targets); err != nil {
return nil, err
}
return targets, nil
}
type updatable interface {
PrimaryKeyColumns() []database.Column
UpdatedAtColumn() database.Column
qualifiedTableName() string
}
func updateOne[Target updatable](ctx context.Context, client database.QueryExecutor, target Target, condition database.Condition, changes ...database.Change) (int64, error) {
if len(changes) == 0 {
return 0, database.ErrNoChanges
}
if err := checkPKCondition(target, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(target.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(target.UpdatedAtColumn(), database.NullInstruction))
}
builder := database.NewStatementBuilder("UPDATE ")
builder.WriteString(target.qualifiedTableName())
builder.WriteString(" SET ")
database.Changes(changes).Write(builder)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
type deletable interface {
PrimaryKeyColumns() []database.Column
qualifiedTableName() string
}
func deleteOne[Target deletable](ctx context.Context, client database.QueryExecutor, target Target, condition database.Condition) (int64, error) {
if err := checkPKCondition(target, condition); err != nil {
return 0, err
}
builder := database.NewStatementBuilder("DELETE FROM ")
builder.WriteString(target.qualifiedTableName())
builder.WriteRune(' ')
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}