Files
zitadel/backend/v3/storage/database/json/json.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

235 lines
4.8 KiB
Go

package json
import (
"encoding/json"
"reflect"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
var _ database.Change = (*jsonChanges)(nil)
type JsonUpdate interface {
writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error
getPathValue() ([]string, string, error)
}
type FieldChange struct {
path []string
value any
}
func NewFieldChange(path []string, value any) JsonUpdate {
return &FieldChange{
path: path,
value: value,
}
}
func (c *FieldChange) getPathValue() ([]string, string, error) {
path, v := c.path, c.value
value, err := json.Marshal(v)
if err != nil {
return nil, "", err
}
return path, string(value), nil
}
func (c *FieldChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("jsonb_set_lax(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString(", true, 'delete_key')")
return nil
}
type ArrayChange struct {
path []string
value any
remove bool
}
func NewArrayChange(path []string, value any, remove bool) JsonUpdate {
return &ArrayChange{
path: path,
value: value,
remove: remove,
}
}
func (c *ArrayChange) getPathValue() ([]string, string, error) {
path, v := c.path, c.value
value, err := json.Marshal(v)
if err != nil {
return nil, "", err
}
return path, string(value), nil
}
func (c *ArrayChange) writeUpdate(builder *database.StatementBuilder, changes jsonChanges, i int) error {
if c.remove {
return c.removeFromArray(builder, changes, i)
}
return c.addToArray(builder, changes, i)
}
func (c *ArrayChange) addToArray(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("zitadel.jsonb_array_append(")
// to mitigate the possibility of having duplicate values in the array
builder.WriteString("zitadel.jsonb_array_remove(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
// zitadel.jsonb_array_remove
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString("::TEXT)")
// zitadel.jsonb_array_append
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' {
builder.WriteString("::TEXT")
} else {
builder.WriteString("::NUMERIC")
}
builder.WriteString(")")
return nil
}
func (c *ArrayChange) removeFromArray(builder *database.StatementBuilder, changes jsonChanges, i int) error {
path, value, err := c.getPathValue()
if err != nil {
return err
}
builder.WriteString("zitadel.jsonb_array_remove(")
if i < 0 {
changes.column.WriteQualified(builder)
} else {
err := changes.changes[i].writeUpdate(builder, changes, i-1)
if err != nil {
return err
}
}
builder.WriteString(", ")
builder.WriteArg(path)
builder.WriteString(", ")
builder.WriteArg(value)
builder.WriteString("::TEXT")
builder.WriteString(")")
return nil
}
var _ JsonUpdate = (*ArrayChange)(nil)
type jsonChanges struct {
column database.Column
changes []JsonUpdate
}
var _ database.Change = (*jsonChanges)(nil)
func NewJsonChanges(col database.Column, changes ...JsonUpdate) database.Change {
return &jsonChanges{
column: col,
changes: changes,
}
}
// WriteArg implements [gomock.Matcher].
func (c *jsonChanges) Matches(x any) bool {
toMatch, ok := x.(*jsonChanges)
if !ok {
return false
}
if !c.column.Equals(toMatch.column) {
return false
}
if len(c.changes) != len(toMatch.changes) {
return false
}
for _, c1 := range c.changes {
found := false
for _, c2 := range toMatch.changes {
if reflect.DeepEqual(c1, c2) {
found = true
}
}
if !found {
return false
}
}
return true
}
// WriteArg implements [database.Change].
func (c *jsonChanges) String() string {
return "database.json.jsonChange"
}
// WriteArg implements [database.Change].
func (c jsonChanges) Write(builder *database.StatementBuilder) error {
c.column.WriteUnqualified(builder)
builder.WriteString(" = ")
if c.changes == nil {
return nil
}
return c.changes[len(c.changes)-1].writeUpdate(builder, c, len(c.changes)-2)
}
// WriteArg implements [database.Change].
func (c jsonChanges) IsOnColumn(col database.Column) bool {
return c.column.Equals(col)
}
// WriteArg implements [database.Change].
func (c jsonChanges) WriteArg(builder *database.StatementBuilder) {
c.column.WriteArg(builder)
}