Files
zitadel/backend/v3/storage/database/repository/repository.go
T
5df8a97f9d feat(repository): implement user repository (#10967)
## Summary

This PR introduces the relational user repository for backend v3,
including schema, repository APIs, reducers/projections, and tests for
both human and machine users.

## What changed

* Added user-focused relational migrations (users, verifications,
metadata, PATs, machine keys, passkeys, identity provider links) and
related FK wiring.
* Implemented/expanded repository domain contracts and storage
implementations for human and machine user flows.
* Added support for machine secret handling and aligned access token
type modeling.
* Extended user domain models (verifications, password/email/phone/TOTP
state, passkeys, metadata, invite handling, identity provider links).
* Updated projection/reduction coverage and integration tests for key
user lifecycle events.
* Regenerated and updated mocks/enums plus supporting database
change/column utilities.

## Notes for reviewers

* This PR is large because it establishes the foundational relational
user storage layer and related test scaffolding. Sorry for that.

## Closes

Closes #10218
Closes #10224
Closes #10225
Closes #10209
Closes #10802

---------

Co-authored-by: Iraq Jaber <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
2026-02-23 16:03:47 +01:00

113 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 ")
if err := database.Changes(changes).Write(builder); err != nil {
return 0, err
}
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()...)
}