This commit is contained in:
adlerhurst
2025-05-08 07:42:53 +02:00
parent 6ba86bc67b
commit 8ba497cb87
166 changed files with 700 additions and 10922 deletions

View File

@@ -0,0 +1,33 @@
package orgv2
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
cmd := domain.NewAddOrgCommand(
req.GetName(),
addOrgAdminToCommand(req.GetAdmins()...)...,
)
err = domain.Invoke(ctx, cmd)
if err != nil {
return nil, err
}
return &org.AddOrganizationResponse{
OrganizationId: cmd.ID,
}, nil
}
func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
cmds := make([]*domain.AddMemberCommand, len(admins))
for i, admin := range admins {
cmds[i] = &domain.AddMemberCommand{
UserID: admin.GetUserId(),
Roles: admin.GetRoles(),
}
}
return cmds
}

View File

@@ -0,0 +1,19 @@
package orgv2
import (
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
var (
logger logging.Logger
tracer tracing.Tracer
)
func SetLogger(l logging.Logger) {
logger = l
}
func SetTracer(t tracing.Tracer) {
tracer = t
}

View File

@@ -3,6 +3,8 @@ package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Instance struct {
@@ -19,16 +21,43 @@ func (i *Instance) Keys(index string) (key []string) {
return []string{}
}
type InstanceRepository interface {
ByID(ctx context.Context, id string) (*Instance, error)
Create(ctx context.Context, instance *Instance) error
On(id string) InstanceOperation
type instanceColumns interface {
// IDColumn returns the column for the id field.
IDColumn() database.Column
// NameColumn returns the column for the name field.
NameColumn() database.Column
// CreatedAtColumn returns the column for the created at field.
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the updated at field.
UpdatedAtColumn() database.Column
// DeletedAtColumn returns the column for the deleted at field.
DeletedAtColumn() database.Column
}
type InstanceOperation interface {
AdminRepository
Update(ctx context.Context, instance *Instance) error
Delete(ctx context.Context) error
type instanceConditions interface {
// IDCondition returns an equal filter on the id field.
IDCondition(instanceID string) database.Condition
// NameCondition returns a filter on the name field.
NameCondition(op database.TextOperation, name string) database.Condition
}
type instanceChanges interface {
// SetName sets the name column.
SetName(name string) database.Change
}
type InstanceRepository interface {
instanceColumns
instanceConditions
instanceChanges
Member() MemberRepository
Get(ctx context.Context, opts ...database.QueryOption) (*Instance, error)
Create(ctx context.Context, instance *Instance) error
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
Delete(ctx context.Context, condition database.Condition) error
}
type CreateInstance struct {

View File

@@ -3,33 +3,95 @@ package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type OrgState uint8
const (
OrgStateActive OrgState = iota + 1
OrgStateInactive
)
type Org struct {
ID string `json:"id"`
Name string `json:"name"`
State OrgState `json:"state"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type orgColumns interface {
// InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() database.Column
// IDColumn returns the column for the id field.
IDColumn() database.Column
// NameColumn returns the column for the name field.
NameColumn() database.Column
// StateColumn returns the column for the state field.
StateColumn() database.Column
// CreatedAtColumn returns the column for the created at field.
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the updated at field.
UpdatedAtColumn() database.Column
// DeletedAtColumn returns the column for the deleted at field.
DeletedAtColumn() database.Column
}
type orgConditions interface {
// InstanceIDCondition returns an equal filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
// IDCondition returns an equal filter on the id field.
IDCondition(orgID string) database.Condition
// NameCondition returns a filter on the name field.
NameCondition(op database.TextOperation, name string) database.Condition
// StateCondition returns a filter on the state field.
StateCondition(op database.NumberOperation, state OrgState) database.Condition
}
type orgChanges interface {
// SetName sets the name column.
SetName(name string) database.Change
// SetState sets the state column.
SetState(state OrgState) database.Change
}
type OrgRepository interface {
ByID(ctx context.Context, orgID string) (*Org, error)
orgColumns
orgConditions
orgChanges
// Member returns the admin repository.
Member() MemberRepository
// Domain returns the domain repository.
Domain() DomainRepository
// Get returns an org based on the given condition.
Get(ctx context.Context, opts ...database.QueryOption) (*Org, error)
// List returns a list of orgs based on the given condition.
List(ctx context.Context, opts ...database.QueryOption) ([]*Org, error)
// Create creates a new org.
Create(ctx context.Context, org *Org) error
On(id string) OrgOperation
// Delete removes orgs based on the given condition.
Delete(ctx context.Context, condition database.Condition) error
// Update executes the given changes based on the given condition.
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
}
type OrgOperation interface {
AdminRepository
MemberRepository
DomainRepository
Update(ctx context.Context, org *Org) error
Delete(ctx context.Context) error
}
type AdminRepository interface {
AddAdmin(ctx context.Context, userID string, roles []string) error
SetAdminRoles(ctx context.Context, userID string, roles []string) error
RemoveAdmin(ctx context.Context, userID string) error
type MemberRepository interface {
AddMember(ctx context.Context, orgID, userID string, roles []string) error
SetMemberRoles(ctx context.Context, orgID, userID string, roles []string) error
RemoveMember(ctx context.Context, orgID, userID string) error
}
type DomainRepository interface {

View File

@@ -2,15 +2,17 @@ package domain
import (
"context"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
type AddOrgCommand struct {
ID string `json:"id"`
Name string `json:"name"`
Admins []AddAdminCommand `json:"admins"`
ID string `json:"id"`
Name string `json:"name"`
Admins []*AddMemberCommand `json:"admins"`
}
func NewAddOrgCommand(name string, admins ...AddAdminCommand) *AddOrgCommand {
func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
return &AddOrgCommand{
Name: name,
Admins: admins,
@@ -39,11 +41,31 @@ func (cmd *AddOrgCommand) Execute(ctx context.Context, opts *CommandOpts) (err e
return err
}
for _, admin := range cmd.Admins {
admin.orgID = cmd.ID
if err = opts.Invoke(ctx, admin); err != nil {
return err
}
}
return nil
}
// Events implements [eventer].
func (cmd *AddOrgCommand) Events() []*eventstore.Event {
return []*eventstore.Event{
{
AggregateType: "org",
AggregateID: cmd.ID,
Type: "org.added",
Payload: cmd,
},
}
}
var (
_ Commander = (*AddOrgCommand)(nil)
_ eventer = (*AddOrgCommand)(nil)
)
func (cmd *AddOrgCommand) ensureID() (err error) {
@@ -54,21 +76,36 @@ func (cmd *AddOrgCommand) ensureID() (err error) {
return err
}
type AddAdminCommand struct {
type AddMemberCommand struct {
orgID string
UserID string `json:"userId"`
Roles []string `json:"roles"`
}
// Execute implements Commander.
func (a *AddAdminCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
func (a *AddMemberCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
close, err := opts.EnsureTx(ctx)
if err != nil {
return err
}
defer func() { err = close(ctx, err) }()
return nil
return orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles)
}
// Events implements [eventer].
func (a *AddMemberCommand) Events() []*eventstore.Event {
return []*eventstore.Event{
{
AggregateType: "org",
AggregateID: a.UserID,
Type: "member.added",
Payload: a,
},
}
}
var (
_ Commander = (*AddAdminCommand)(nil)
_ Commander = (*AddMemberCommand)(nil)
_ eventer = (*AddMemberCommand)(nil)
)

View File

@@ -3,11 +3,11 @@
//
// Generated by this command:
//
// mockgen -typed -package mock -destination ./mock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction
// mockgen -typed -package dbmock -destination ./dbmock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction
//
// Package mock is a generated GoMock package.
package mock
// Package dbmock is a generated GoMock package.
package dbmock
import (
context "context"

View File

@@ -8,8 +8,8 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"github.com/zitadel/zitadel/backend/storage/database"
"github.com/zitadel/zitadel/backend/storage/database/dialect/postgres"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
)
type Hook struct {

View File

@@ -1,3 +1,3 @@
package database
//go:generate mockgen -typed -package mock -destination ./mock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction
//go:generate mockgen -typed -package dbmock -destination ./dbmock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction

View File

@@ -49,7 +49,7 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp
case TextOperationEqual, TextOperationNotEqual:
col.Write(builder)
builder.WriteString(textOperations[op])
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
case TextOperationEqualIgnoreCase, TextOperationNotEqualIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
ignoreCaseCol.WriteIgnoreCase(builder)
@@ -60,12 +60,12 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp
}
builder.WriteString(textOperations[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
builder.WriteString(")")
case TextOperationStartsWith:
col.Write(builder)
builder.WriteString(textOperations[op])
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
builder.WriteString(" || '%'")
case TextOperationStartsWithIgnoreCase:
if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok {
@@ -77,7 +77,7 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp
}
builder.WriteString(textOperations[op])
builder.WriteString("LOWER(")
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
builder.WriteString(")")
builder.WriteString(" || '%'")
default:
@@ -118,7 +118,7 @@ var numberOperations = map[NumberOperation]string{
func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value T) {
col.Write(builder)
builder.WriteString(numberOperations[op])
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
}
type Boolean interface {
@@ -135,5 +135,5 @@ const (
func writeBooleanOperation[T Boolean](builder *StatementBuilder, col Column, value T) {
col.Write(builder)
builder.WriteString(" IS ")
builder.WriteString(builder.AppendArg(value))
builder.WriteArg(value)
}

View File

@@ -1,160 +0,0 @@
package repository
import (
"fmt"
"github.com/zitadel/zitadel/backend/v3/domain"
)
type field interface {
fmt.Stringer
}
type fieldDescriptor struct {
schema string
table string
name string
}
func (f fieldDescriptor) String() string {
return f.schema + "." + f.table + "." + f.name
}
type ignoreCaseFieldDescriptor struct {
fieldDescriptor
fieldNameSuffix string
}
func (f ignoreCaseFieldDescriptor) String() string {
return f.fieldDescriptor.String() + f.fieldNameSuffix
}
type textFieldDescriptor struct {
field
isIgnoreCase bool
}
type clause[Op domain.Operation] struct {
field field
op Op
}
const (
schema = "zitadel"
userTable = "users"
)
var userFields = map[domain.UserField]field{
domain.UserFieldInstanceID: fieldDescriptor{
schema: schema,
table: userTable,
name: "instance_id",
},
domain.UserFieldOrgID: fieldDescriptor{
schema: schema,
table: userTable,
name: "org_id",
},
domain.UserFieldID: fieldDescriptor{
schema: schema,
table: userTable,
name: "id",
},
domain.UserFieldUsername: textFieldDescriptor{
field: ignoreCaseFieldDescriptor{
fieldDescriptor: fieldDescriptor{
schema: schema,
table: userTable,
name: "username",
},
fieldNameSuffix: "_lower",
},
},
domain.UserHumanFieldEmail: textFieldDescriptor{
field: ignoreCaseFieldDescriptor{
fieldDescriptor: fieldDescriptor{
schema: schema,
table: userTable,
name: "email",
},
fieldNameSuffix: "_lower",
},
},
domain.UserHumanFieldEmailVerified: fieldDescriptor{
schema: schema,
table: userTable,
name: "email_is_verified",
},
}
type textClause[V domain.Text] struct {
clause[domain.TextOperation]
value V
}
var textOp map[domain.TextOperation]string = map[domain.TextOperation]string{
domain.TextOperationEqual: " = ",
domain.TextOperationNotEqual: " <> ",
domain.TextOperationStartsWith: " LIKE ",
domain.TextOperationStartsWithIgnoreCase: " LIKE ",
}
func (tc textClause[V]) Write(stmt *statement) {
placeholder := stmt.appendArg(tc.value)
var (
left, right string
)
switch tc.clause.op {
case domain.TextOperationEqual:
left = tc.clause.field.String()
right = placeholder
case domain.TextOperationNotEqual:
left = tc.clause.field.String()
right = placeholder
case domain.TextOperationStartsWith:
left = tc.clause.field.String()
right = placeholder + "%"
case domain.TextOperationStartsWithIgnoreCase:
left = tc.clause.field.String()
if _, ok := tc.clause.field.(ignoreCaseFieldDescriptor); !ok {
left = "LOWER(" + left + ")"
}
right = "LOWER(" + placeholder + "%)"
}
stmt.builder.WriteString(left)
stmt.builder.WriteString(textOp[tc.clause.op])
stmt.builder.WriteString(right)
}
type boolClause[V domain.Bool] struct {
clause[domain.BoolOperation]
value V
}
func (bc boolClause[V]) Write(stmt *statement) {
if !bc.value {
stmt.builder.WriteString("NOT ")
}
stmt.builder.WriteString(bc.clause.field.String())
}
type numberClause[V domain.Number] struct {
clause[domain.NumberOperation]
value V
}
var numberOp map[domain.NumberOperation]string = map[domain.NumberOperation]string{
domain.NumberOperationEqual: " = ",
domain.NumberOperationNotEqual: " <> ",
domain.NumberOperationLessThan: " < ",
domain.NumberOperationLessThanOrEqual: " <= ",
domain.NumberOperationGreaterThan: " > ",
domain.NumberOperationGreaterThanOrEqual: " >= ",
}
func (nc numberClause[V]) Write(stmt *statement) {
stmt.builder.WriteString(nc.clause.field.String())
stmt.builder.WriteString(numberOp[nc.clause.op])
stmt.builder.WriteString(stmt.appendArg(nc.value))
}

View File

@@ -1,45 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/crypto"
)
type cryptoRepo struct {
database.QueryExecutor
}
func Crypto(db database.QueryExecutor) domain.CryptoRepository {
return &cryptoRepo{
QueryExecutor: db,
}
}
const getEncryptionConfigQuery = "SELECT" +
" length" +
", expiry" +
", should_include_lower_letters" +
", should_include_upper_letters" +
", should_include_digits" +
", should_include_symbols" +
" FROM encryption_config"
func (repo *cryptoRepo) GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error) {
var config crypto.GeneratorConfig
row := repo.QueryRow(ctx, getEncryptionConfigQuery)
err := row.Scan(
&config.Length,
&config.Expiry,
&config.IncludeLowerLetters,
&config.IncludeUpperLetters,
&config.IncludeDigits,
&config.IncludeSymbols,
)
if err != nil {
return nil, err
}
return &config, nil
}

View File

@@ -1,7 +0,0 @@
// Repository package provides the database repository for the application.
// It contains the implementation of the [repository pattern](https://martinfowler.com/eaaCatalog/repository.html) for the database.
// funcs which need to interact with the database should create interfaces which are implemented by the
// [query] and [exec] structs respectively their factory methods [Query] and [Execute]. The [query] struct is used for read operations, while the [exec] struct is used for write operations.
package repository

View File

@@ -1,54 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type instance struct {
database.QueryExecutor
}
func Instance(client database.QueryExecutor) domain.InstanceRepository {
return &instance{QueryExecutor: client}
}
func (i *instance) ByID(ctx context.Context, id string) (*domain.Instance, error) {
var instance domain.Instance
err := i.QueryExecutor.QueryRow(ctx, `SELECT id, name, created_at, updated_at, deleted_at FROM instances WHERE id = $1`, id).Scan(
&instance.ID,
&instance.Name,
&instance.CreatedAt,
&instance.UpdatedAt,
&instance.DeletedAt,
)
if err != nil {
return nil, err
}
return &instance, nil
}
const createInstanceStmt = `INSERT INTO instances (id, name) VALUES ($1, $2) RETURNING created_at, updated_at`
// Create implements [domain.InstanceRepository].
func (i *instance) Create(ctx context.Context, instance *domain.Instance) error {
return i.QueryExecutor.QueryRow(ctx, createInstanceStmt,
instance.ID,
instance.Name,
).Scan(
&instance.CreatedAt,
&instance.UpdatedAt,
)
}
// On implements [domain.InstanceRepository].
func (i *instance) On(id string) domain.InstanceOperation {
return &instanceOperation{
QueryExecutor: i.QueryExecutor,
id: id,
}
}
var _ domain.InstanceRepository = (*instance)(nil)

View File

@@ -1,52 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type instanceOperation struct {
database.QueryExecutor
id string
}
const addInstanceAdminStmt = `INSERT INTO instance_admins (instance_id, user_id, roles) VALUES ($1, $2, $3)`
// AddAdmin implements [domain.InstanceOperation].
func (i *instanceOperation) AddAdmin(ctx context.Context, userID string, roles []string) error {
return i.QueryExecutor.Exec(ctx, addInstanceAdminStmt, i.id, userID, roles)
}
// Delete implements [domain.InstanceOperation].
func (i *instanceOperation) Delete(ctx context.Context) error {
return i.QueryExecutor.Exec(ctx, `DELETE FROM instances WHERE id = $1`, i.id)
}
const removeInstanceAdminStmt = `DELETE FROM instance_admins WHERE instance_id = $1 AND user_id = $2`
// RemoveAdmin implements [domain.InstanceOperation].
func (i *instanceOperation) RemoveAdmin(ctx context.Context, userID string) error {
return i.QueryExecutor.Exec(ctx, removeInstanceAdminStmt, i.id, userID)
}
const setInstanceAdminRolesStmt = `UPDATE instance_admins SET roles = $1 WHERE instance_id = $2 AND user_id = $3`
// SetAdminRoles implements [domain.InstanceOperation].
func (i *instanceOperation) SetAdminRoles(ctx context.Context, userID string, roles []string) error {
return i.QueryExecutor.Exec(ctx, setInstanceAdminRolesStmt, roles, i.id, userID)
}
const updateInstanceStmt = `UPDATE instances SET name = $1, updated_at = $2 WHERE id = $3 RETURNING updated_at`
// Update implements [domain.InstanceOperation].
func (i *instanceOperation) Update(ctx context.Context, instance *domain.Instance) error {
return i.QueryExecutor.QueryRow(ctx, updateInstanceStmt,
instance.Name,
instance.UpdatedAt,
i.id,
).Scan(&instance.UpdatedAt)
}
var _ domain.InstanceOperation = (*instanceOperation)(nil)

View File

@@ -0,0 +1,136 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
type org struct {
repository
}
func OrgRepository(client database.QueryExecutor) domain.OrgRepository {
return &org{
repository: repository{
client: client,
},
}
}
// Create implements [domain.OrgRepository].
func (o *org) Create(ctx context.Context, org *domain.Org) error {
panic("unimplemented")
}
// Delete implements [domain.OrgRepository].
func (o *org) Delete(ctx context.Context, condition database.Condition) error {
panic("unimplemented")
}
// Get implements [domain.OrgRepository].
func (o *org) Get(ctx context.Context, opts ...database.QueryOption) (*domain.Org, error) {
panic("unimplemented")
}
// List implements [domain.OrgRepository].
func (o *org) List(ctx context.Context, opts ...database.QueryOption) ([]*domain.Org, error) {
panic("unimplemented")
}
// Update implements [domain.OrgRepository].
func (o *org) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error {
panic("unimplemented")
}
func (o *org) Member() domain.MemberRepository {
return &orgMember{o}
}
func (o *org) Domain() domain.DomainRepository {
return &orgDomain{o}
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetName implements [domain.orgChanges].
func (o *org) SetName(name string) database.Change {
return database.NewChange(o.NameColumn(), name)
}
// SetState implements [domain.orgChanges].
func (o *org) SetState(state domain.OrgState) database.Change {
return database.NewChange(o.StateColumn(), state)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// IDCondition implements [domain.orgConditions].
func (o *org) IDCondition(orgID string) database.Condition {
return database.NewTextCondition(o.IDColumn(), database.TextOperationEqual, orgID)
}
// InstanceIDCondition implements [domain.orgConditions].
func (o *org) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(o.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// NameCondition implements [domain.orgConditions].
func (o *org) NameCondition(op database.TextOperation, name string) database.Condition {
return database.NewTextCondition(o.NameColumn(), op, name)
}
// StateCondition implements [domain.orgConditions].
func (o *org) StateCondition(op database.NumberOperation, state domain.OrgState) database.Condition {
return database.NewNumberCondition(o.StateColumn(), op, state)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// CreatedAtColumn implements [domain.orgColumns].
func (o *org) CreatedAtColumn() database.Column {
return database.NewColumn("created_at")
}
// DeletedAtColumn implements [domain.orgColumns].
func (o *org) DeletedAtColumn() database.Column {
return database.NewColumn("deleted_at")
}
// IDColumn implements [domain.orgColumns].
func (o *org) IDColumn() database.Column {
return database.NewColumn("id")
}
// InstanceIDColumn implements [domain.orgColumns].
func (o *org) InstanceIDColumn() database.Column {
return database.NewColumn("instance_id")
}
// NameColumn implements [domain.orgColumns].
func (o *org) NameColumn() database.Column {
return database.NewColumn("name")
}
// StateColumn implements [domain.orgColumns].
func (o *org) StateColumn() database.Column {
return database.NewColumn("state")
}
// UpdatedAtColumn implements [domain.orgColumns].
func (o *org) UpdatedAtColumn() database.Column {
return database.NewColumn("updated_at")
}
var _ domain.OrgRepository = (*org)(nil)

View File

@@ -0,0 +1,28 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
)
type orgDomain struct {
*org
}
// AddDomain implements [domain.DomainRepository].
func (o *orgDomain) AddDomain(ctx context.Context, domain string) error {
panic("unimplemented")
}
// RemoveDomain implements [domain.DomainRepository].
func (o *orgDomain) RemoveDomain(ctx context.Context, domain string) error {
panic("unimplemented")
}
// SetDomainVerified implements [domain.DomainRepository].
func (o *orgDomain) SetDomainVerified(ctx context.Context, domain string) error {
panic("unimplemented")
}
var _ domain.DomainRepository = (*orgDomain)(nil)

View File

@@ -0,0 +1,28 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
)
type orgMember struct {
*org
}
// AddMember implements [domain.MemberRepository].
func (o *orgMember) AddMember(ctx context.Context, orgID string, userID string, roles []string) error {
panic("unimplemented")
}
// RemoveMember implements [domain.MemberRepository].
func (o *orgMember) RemoveMember(ctx context.Context, orgID string, userID string) error {
panic("unimplemented")
}
// SetMemberRoles implements [domain.MemberRepository].
func (o *orgMember) SetMemberRoles(ctx context.Context, orgID string, userID string, roles []string) error {
panic("unimplemented")
}
var _ domain.MemberRepository = (*orgMember)(nil)

View File

@@ -1,17 +0,0 @@
package repository
import (
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type query struct{ database.Querier }
func Query(querier database.Querier) *query {
return &query{Querier: querier}
}
type executor struct{ database.Executor }
func Execute(exec database.Executor) *executor {
return &executor{Executor: exec}
}

View File

@@ -0,0 +1,8 @@
package repository
import "github.com/zitadel/zitadel/backend/v3/storage/database"
type repository struct {
builder database.StatementBuilder
client database.QueryExecutor
}

View File

@@ -1,21 +0,0 @@
package repository
import "strings"
type statement struct {
builder strings.Builder
args []any
}
func (s *statement) appendArg(arg any) (placeholder string) {
s.args = append(s.args, arg)
return "$" + string(len(s.args))
}
func (s *statement) appendArgs(args ...any) (placeholders []string) {
placeholders = make([]string, len(args))
for i, arg := range args {
placeholders[i] = s.appendArg(arg)
}
return placeholders
}

View File

@@ -1,43 +0,0 @@
package stmt
import "fmt"
type Column[T any] interface {
fmt.Stringer
statementApplier[T]
scanner(t *T) any
}
type columnDescriptor[T any] struct {
name string
scan func(*T) any
}
func (cd columnDescriptor[T]) scanner(t *T) any {
return cd.scan(t)
}
// Apply implements [Column].
func (f columnDescriptor[T]) Apply(stmt *statement[T]) {
stmt.builder.WriteString(stmt.columnPrefix())
stmt.builder.WriteString(f.String())
}
// String implements [Column].
func (f columnDescriptor[T]) String() string {
return f.name
}
var _ Column[any] = (*columnDescriptor[any])(nil)
type ignoreCaseColumnDescriptor[T any] struct {
columnDescriptor[T]
fieldNameSuffix string
}
func (f ignoreCaseColumnDescriptor[T]) ApplyIgnoreCase(stmt *statement[T]) {
stmt.builder.WriteString(f.String())
stmt.builder.WriteString(f.fieldNameSuffix)
}
var _ Column[any] = (*ignoreCaseColumnDescriptor[any])(nil)

View File

@@ -1,97 +0,0 @@
package stmt
import "fmt"
type statementApplier[T any] interface {
// Apply writes the statement to the builder.
Apply(stmt *statement[T])
}
type Condition[T any] interface {
statementApplier[T]
}
type op interface {
TextOperation | NumberOperation | ListOperation
fmt.Stringer
}
type operation[T any, O op] struct {
o O
}
func (o operation[T, O]) String() string {
return o.o.String()
}
func (o operation[T, O]) Apply(stmt *statement[T]) {
stmt.builder.WriteString(o.o.String())
}
type condition[V, T any, OP op] struct {
field Column[T]
op OP
value V
}
func (c *condition[V, T, OP]) Apply(stmt *statement[T]) {
// placeholder := stmt.appendArg(c.value)
stmt.builder.WriteString(stmt.columnPrefix())
stmt.builder.WriteString(c.field.String())
// stmt.builder.WriteString(c.op)
// stmt.builder.WriteString(placeholder)
}
type and[T any] struct {
conditions []Condition[T]
}
func And[T any](conditions ...Condition[T]) *and[T] {
return &and[T]{
conditions: conditions,
}
}
// Apply implements [Condition].
func (a *and[T]) Apply(stmt *statement[T]) {
if len(a.conditions) > 1 {
stmt.builder.WriteString("(")
defer stmt.builder.WriteString(")")
}
for i, condition := range a.conditions {
if i > 0 {
stmt.builder.WriteString(" AND ")
}
condition.Apply(stmt)
}
}
var _ Condition[any] = (*and[any])(nil)
type or[T any] struct {
conditions []Condition[T]
}
func Or[T any](conditions ...Condition[T]) *or[T] {
return &or[T]{
conditions: conditions,
}
}
// Apply implements [Condition].
func (o *or[T]) Apply(stmt *statement[T]) {
if len(o.conditions) > 1 {
stmt.builder.WriteString("(")
defer stmt.builder.WriteString(")")
}
for i, condition := range o.conditions {
if i > 0 {
stmt.builder.WriteString(" OR ")
}
condition.Apply(stmt)
}
}
var _ Condition[any] = (*or[any])(nil)

View File

@@ -1,71 +0,0 @@
package stmt
type ListEntry interface {
Number | Text | any
}
type ListCondition[E ListEntry, T any] struct {
condition[[]E, T, ListOperation]
}
func (lc *ListCondition[E, T]) Apply(stmt *statement[T]) {
placeholder := stmt.appendArg(lc.value)
switch lc.op {
case ListOperationEqual, ListOperationNotEqual:
lc.field.Apply(stmt)
operation[T, ListOperation]{lc.op}.Apply(stmt)
stmt.builder.WriteString(placeholder)
case ListOperationContainsAny, ListOperationContainsAll:
lc.field.Apply(stmt)
operation[T, ListOperation]{lc.op}.Apply(stmt)
stmt.builder.WriteString(placeholder)
case ListOperationNotContainsAny, ListOperationNotContainsAll:
stmt.builder.WriteString("NOT (")
lc.field.Apply(stmt)
operation[T, ListOperation]{lc.op}.Apply(stmt)
stmt.builder.WriteString(placeholder)
stmt.builder.WriteString(")")
default:
panic("unknown list operation")
}
}
type ListOperation uint8
const (
// ListOperationEqual checks if the arrays are equal including the order of the elements
ListOperationEqual ListOperation = iota + 1
// ListOperationNotEqual checks if the arrays are not equal including the order of the elements
ListOperationNotEqual
// ListOperationContains checks if the array column contains all the values of the specified array
ListOperationContainsAll
// ListOperationContainsAny checks if the arrays have at least one value in common
ListOperationContainsAny
// ListOperationContainsAll checks if the array column contains all the values of the specified array
// ListOperationNotContainsAll checks if the specified array is not contained by the column
ListOperationNotContainsAll
// ListOperationNotContainsAny checks if the arrays column contains none of the values of the specified array
ListOperationNotContainsAny
)
var listOperations = map[ListOperation]string{
// ListOperationEqual checks if the lists are equal
ListOperationEqual: " = ",
// ListOperationNotEqual checks if the lists are not equal
ListOperationNotEqual: " <> ",
// ListOperationContainsAny checks if the arrays have at least one value in common
ListOperationContainsAny: " && ",
// ListOperationContainsAll checks if the array column contains all the values of the specified array
ListOperationContainsAll: " @> ",
// ListOperationNotContainsAny checks if the arrays column contains none of the values of the specified array
ListOperationNotContainsAny: " && ", // Base operator for NOT (A && B)
// ListOperationNotContainsAll checks if the array column is not contained by the specified array
ListOperationNotContainsAll: " <@ ", // Base operator for NOT (A <@ B)
}
func (lo ListOperation) String() string {
return listOperations[lo]
}

View File

@@ -1,61 +0,0 @@
package stmt
import (
"time"
"golang.org/x/exp/constraints"
)
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration
}
type between[N Number] struct {
min, max N
}
type NumberBetween[V Number, T any] struct {
condition[between[V], T, NumberOperation]
}
func (nb *NumberBetween[V, T]) Apply(stmt *statement[T]) {
nb.field.Apply(stmt)
stmt.builder.WriteString(" BETWEEN ")
stmt.builder.WriteString(stmt.appendArg(nb.value.min))
stmt.builder.WriteString(" AND ")
stmt.builder.WriteString(stmt.appendArg(nb.value.max))
}
type NumberCondition[V Number, T any] struct {
condition[V, T, NumberOperation]
}
func (nc *NumberCondition[V, T]) Apply(stmt *statement[T]) {
nc.field.Apply(stmt)
operation[T, NumberOperation]{nc.op}.Apply(stmt)
stmt.builder.WriteString(stmt.appendArg(nc.value))
}
type NumberOperation uint8
const (
NumberOperationEqual NumberOperation = iota + 1
NumberOperationNotEqual
NumberOperationLessThan
NumberOperationLessThanOrEqual
NumberOperationGreaterThan
NumberOperationGreaterThanOrEqual
)
var numberOperations = map[NumberOperation]string{
NumberOperationEqual: " = ",
NumberOperationNotEqual: " <> ",
NumberOperationLessThan: " < ",
NumberOperationLessThanOrEqual: " <= ",
NumberOperationGreaterThan: " > ",
NumberOperationGreaterThanOrEqual: " >= ",
}
func (no NumberOperation) String() string {
return numberOperations[no]
}

View File

@@ -1,104 +0,0 @@
package stmt
import (
"fmt"
"strings"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type statement[T any] struct {
builder strings.Builder
client database.QueryExecutor
columns []Column[T]
schema string
table string
alias string
condition Condition[T]
limit uint32
offset uint32
// order by fieldname and sort direction false for asc true for desc
// orderBy SortingColumns[C]
args []any
existingArgs map[any]string
}
func (s *statement[T]) scanners(t *T) []any {
scanners := make([]any, len(s.columns))
for i, column := range s.columns {
scanners[i] = column.scanner(t)
}
return scanners
}
func (s *statement[T]) query() string {
s.builder.WriteString(`SELECT `)
for i, column := range s.columns {
if i > 0 {
s.builder.WriteString(", ")
}
column.Apply(s)
}
s.builder.WriteString(` FROM `)
s.builder.WriteString(s.schema)
s.builder.WriteRune('.')
s.builder.WriteString(s.table)
if s.alias != "" {
s.builder.WriteString(" AS ")
s.builder.WriteString(s.alias)
}
s.builder.WriteString(` WHERE `)
s.condition.Apply(s)
if s.limit > 0 {
s.builder.WriteString(` LIMIT `)
s.builder.WriteString(s.appendArg(s.limit))
}
if s.offset > 0 {
s.builder.WriteString(` OFFSET `)
s.builder.WriteString(s.appendArg(s.offset))
}
return s.builder.String()
}
// func (s *statement[T]) Where(condition Condition[T]) *statement[T] {
// s.condition = condition
// return s
// }
// func (s *statement[T]) Limit(limit uint32) *statement[T] {
// s.limit = limit
// return s
// }
// func (s *statement[T]) Offset(offset uint32) *statement[T] {
// s.offset = offset
// return s
// }
func (s *statement[T]) columnPrefix() string {
if s.alias != "" {
return s.alias + "."
}
return s.schema + "." + s.table + "."
}
func (s *statement[T]) appendArg(arg any) string {
if s.existingArgs == nil {
s.existingArgs = make(map[any]string)
}
if existing, ok := s.existingArgs[arg]; ok {
return existing
}
s.args = append(s.args, arg)
placeholder := fmt.Sprintf("$%d", len(s.args))
s.existingArgs[arg] = placeholder
return placeholder
}

View File

@@ -1,18 +0,0 @@
package stmt_test
import (
"context"
"testing"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt"
)
func Test_Bla(t *testing.T) {
stmt.User(nil).Where(
stmt.Or(
stmt.UserIDCondition("123"),
stmt.UserIDCondition("123"),
stmt.UserUsernameCondition(stmt.TextOperationEqualIgnoreCase, "test"),
),
).Limit(1).Offset(1).Get(context.Background())
}

View File

@@ -1,72 +0,0 @@
package stmt
type Text interface {
~string | ~[]byte
}
type TextCondition[V Text, T any] struct {
condition[V, T, TextOperation]
}
func (tc *TextCondition[V, T]) Apply(stmt *statement[T]) {
placeholder := stmt.appendArg(tc.value)
switch tc.op {
case TextOperationEqual, TextOperationNotEqual:
tc.field.Apply(stmt)
operation[T, TextOperation]{tc.op}.Apply(stmt)
stmt.builder.WriteString(placeholder)
case TextOperationEqualIgnoreCase:
if desc, ok := tc.field.(ignoreCaseColumnDescriptor[T]); ok {
desc.ApplyIgnoreCase(stmt)
} else {
stmt.builder.WriteString("LOWER(")
tc.field.Apply(stmt)
stmt.builder.WriteString(")")
}
operation[T, TextOperation]{tc.op}.Apply(stmt)
stmt.builder.WriteString("LOWER(")
stmt.builder.WriteString(placeholder)
stmt.builder.WriteString(")")
case TextOperationStartsWith:
tc.field.Apply(stmt)
operation[T, TextOperation]{tc.op}.Apply(stmt)
stmt.builder.WriteString(placeholder)
stmt.builder.WriteString("|| '%'")
case TextOperationStartsWithIgnoreCase:
if desc, ok := tc.field.(ignoreCaseColumnDescriptor[T]); ok {
desc.ApplyIgnoreCase(stmt)
} else {
stmt.builder.WriteString("LOWER(")
tc.field.Apply(stmt)
stmt.builder.WriteString(")")
}
operation[T, TextOperation]{tc.op}.Apply(stmt)
stmt.builder.WriteString("LOWER(")
stmt.builder.WriteString(placeholder)
stmt.builder.WriteString(")")
stmt.builder.WriteString("|| '%'")
}
}
type TextOperation uint8
const (
TextOperationEqual TextOperation = iota + 1
TextOperationEqualIgnoreCase
TextOperationNotEqual
TextOperationStartsWith
TextOperationStartsWithIgnoreCase
)
var textOperations = map[TextOperation]string{
TextOperationEqual: " = ",
TextOperationEqualIgnoreCase: " = ",
TextOperationNotEqual: " <> ",
TextOperationStartsWith: " LIKE ",
TextOperationStartsWithIgnoreCase: " LIKE ",
}
func (to TextOperation) String() string {
return textOperations[to]
}

View File

@@ -1,193 +0,0 @@
package stmt
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type userStatement struct {
statement[domain.User]
}
func User(client database.QueryExecutor) *userStatement {
return &userStatement{
statement: statement[domain.User]{
schema: "zitadel",
table: "users",
alias: "u",
client: client,
columns: []Column[domain.User]{
userColumns[UserInstanceID],
userColumns[UserOrgID],
userColumns[UserColumnID],
userColumns[UserColumnUsername],
userColumns[UserCreatedAt],
userColumns[UserUpdatedAt],
userColumns[UserDeletedAt],
},
},
}
}
func (s *userStatement) Where(condition Condition[domain.User]) *userStatement {
s.condition = condition
return s
}
func (s *userStatement) Limit(limit uint32) *userStatement {
s.limit = limit
return s
}
func (s *userStatement) Offset(offset uint32) *userStatement {
s.offset = offset
return s
}
func (s *userStatement) Get(ctx context.Context) (*domain.User, error) {
var user domain.User
err := s.client.QueryRow(ctx, s.query(), s.statement.args...).Scan(s.scanners(&user)...)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *userStatement) List(ctx context.Context) ([]*domain.User, error) {
var users []*domain.User
rows, err := s.client.Query(ctx, s.query(), s.statement.args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var user domain.User
err = rows.Scan(s.scanners(&user)...)
if err != nil {
return nil, err
}
users = append(users, &user)
}
return users, nil
}
func (s *userStatement) SetUsername(ctx context.Context, username string) error {
return nil
}
type UserColumn uint8
var (
userColumns map[UserColumn]Column[domain.User] = map[UserColumn]Column[domain.User]{
UserInstanceID: columnDescriptor[domain.User]{
name: "instance_id",
scan: func(u *domain.User) any {
return &u.InstanceID
},
},
UserOrgID: columnDescriptor[domain.User]{
name: "org_id",
scan: func(u *domain.User) any {
return &u.OrgID
},
},
UserColumnID: columnDescriptor[domain.User]{
name: "id",
scan: func(u *domain.User) any {
return &u.ID
},
},
UserColumnUsername: ignoreCaseColumnDescriptor[domain.User]{
columnDescriptor: columnDescriptor[domain.User]{
name: "username",
scan: func(u *domain.User) any {
return &u.Username
},
},
fieldNameSuffix: "_lower",
},
UserCreatedAt: columnDescriptor[domain.User]{
name: "created_at",
scan: func(u *domain.User) any {
return &u.CreatedAt
},
},
UserUpdatedAt: columnDescriptor[domain.User]{
name: "updated_at",
scan: func(u *domain.User) any {
return &u.UpdatedAt
},
},
UserDeletedAt: columnDescriptor[domain.User]{
name: "deleted_at",
scan: func(u *domain.User) any {
return &u.DeletedAt
},
},
}
humanColumns = map[UserColumn]Column[domain.User]{
UserHumanColumnEmail: ignoreCaseColumnDescriptor[domain.User]{
columnDescriptor: columnDescriptor[domain.User]{
name: "email",
scan: func(u *domain.User) any {
human, ok := u.Traits.(*domain.Human)
if !ok {
return nil
}
if human.Email == nil {
human.Email = new(domain.Email)
}
return &human.Email.Address
},
},
fieldNameSuffix: "_lower",
},
UserHumanColumnEmailVerified: columnDescriptor[domain.User]{
name: "email_is_verified",
scan: func(u *domain.User) any {
human, ok := u.Traits.(*domain.Human)
if !ok {
return nil
}
if human.Email == nil {
human.Email = new(domain.Email)
}
return &human.Email.IsVerified
},
},
}
machineColumns = map[UserColumn]Column[domain.User]{
UserMachineDescription: columnDescriptor[domain.User]{
name: "description",
scan: func(u *domain.User) any {
machine, ok := u.Traits.(*domain.Machine)
if !ok {
return nil
}
if machine == nil {
machine = new(domain.Machine)
}
return &machine.Description
},
},
}
)
const (
UserInstanceID UserColumn = iota + 1
UserOrgID
UserColumnID
UserColumnUsername
UserHumanColumnEmail
UserHumanColumnEmailVerified
UserMachineDescription
UserCreatedAt
UserUpdatedAt
UserDeletedAt
)

View File

@@ -1,23 +0,0 @@
package stmt
import "github.com/zitadel/zitadel/backend/v3/domain"
func UserIDCondition(id string) *TextCondition[string, domain.User] {
return &TextCondition[string, domain.User]{
condition: condition[string, domain.User, TextOperation]{
field: userColumns[UserColumnID],
op: TextOperationEqual,
value: id,
},
}
}
func UserUsernameCondition(op TextOperation, username string) *TextCondition[string, domain.User] {
return &TextCondition[string, domain.User]{
condition: condition[string, domain.User, TextOperation]{
field: userColumns[UserColumnUsername],
op: op,
value: username,
},
}
}

View File

@@ -1,135 +0,0 @@
package stmt
// type table struct {
// schema string
// name string
// possibleJoins []*join
// columns []*col
// }
// type col struct {
// *table
// name string
// }
// type join struct {
// *table
// on []*joinColumns
// }
// type joinColumns struct {
// left, right *col
// }
// var (
// userTable = &table{
// schema: "zitadel",
// name: "users",
// }
// userColumns = []*col{
// userInstanceIDColumn,
// userOrgIDColumn,
// userIDColumn,
// userUsernameColumn,
// }
// userInstanceIDColumn = &col{
// table: userTable,
// name: "instance_id",
// }
// userOrgIDColumn = &col{
// table: userTable,
// name: "org_id",
// }
// userIDColumn = &col{
// table: userTable,
// name: "id",
// }
// userUsernameColumn = &col{
// table: userTable,
// name: "username",
// }
// userJoins = []*join{
// {
// table: instanceTable,
// on: []*joinColumns{
// {
// left: instanceIDColumn,
// right: userInstanceIDColumn,
// },
// },
// },
// {
// table: orgTable,
// on: []*joinColumns{
// {
// left: orgIDColumn,
// right: userOrgIDColumn,
// },
// },
// },
// }
// )
// var (
// instanceTable = &table{
// schema: "zitadel",
// name: "instances",
// }
// instanceColumns = []*col{
// instanceIDColumn,
// instanceNameColumn,
// }
// instanceIDColumn = &col{
// table: instanceTable,
// name: "id",
// }
// instanceNameColumn = &col{
// table: instanceTable,
// name: "name",
// }
// )
// var (
// orgTable = &table{
// schema: "zitadel",
// name: "orgs",
// }
// orgColumns = []*col{
// orgInstanceIDColumn,
// orgIDColumn,
// orgNameColumn,
// }
// orgInstanceIDColumn = &col{
// table: orgTable,
// name: "instance_id",
// }
// orgIDColumn = &col{
// table: orgTable,
// name: "id",
// }
// orgNameColumn = &col{
// table: orgTable,
// name: "name",
// }
// )
// func init() {
// instanceTable.columns = instanceColumns
// userTable.columns = userColumns
// userTable.possibleJoins = []join{
// {
// table: userTable,
// on: []joinColumns{
// {
// left: userIDColumn,
// right: userIDColumn,
// },
// },
// },
// }
// }

View File

@@ -1,55 +0,0 @@
package v3
type Column interface {
Name() string
Write(builder statementBuilder)
}
type ignoreCaseColumn interface {
Column
WriteIgnoreCase(builder statementBuilder)
}
var (
columnNameID = "id"
columnNameName = "name"
columnNameCreatedAt = "created_at"
columnNameUpdatedAt = "updated_at"
columnNameDeletedAt = "deleted_at"
columnNameInstanceID = "instance_id"
columnNameOrgID = "org_id"
)
type column struct {
table Table
name string
}
// Write implements Column.
func (c *column) Write(builder statementBuilder) {
c.table.writeOn(builder)
builder.writeRune('.')
builder.writeString(c.name)
}
// Name implements [Column].
func (c *column) Name() string {
return c.name
}
var _ Column = (*column)(nil)
type columnIgnoreCase struct {
column
suffix string
}
// WriteIgnoreCase implements ignoreCaseColumn.
func (c *columnIgnoreCase) WriteIgnoreCase(builder statementBuilder) {
c.Write(builder)
builder.writeString(c.suffix)
}
var _ ignoreCaseColumn = (*columnIgnoreCase)(nil)

View File

@@ -1,182 +0,0 @@
package v3
type statementBuilder interface {
write([]byte)
writeString(string)
writeRune(rune)
appendArg(any) (placeholder string)
table() Table
}
type Condition interface {
writeOn(builder statementBuilder)
}
type and struct {
conditions []Condition
}
func And(conditions ...Condition) *and {
return &and{conditions: conditions}
}
// writeOn implements [Condition].
func (a *and) writeOn(builder statementBuilder) {
if len(a.conditions) > 1 {
builder.writeString("(")
defer builder.writeString(")")
}
for i, condition := range a.conditions {
if i > 0 {
builder.writeString(" AND ")
}
condition.writeOn(builder)
}
}
var _ Condition = (*and)(nil)
type or struct {
conditions []Condition
}
func Or(conditions ...Condition) *or {
return &or{conditions: conditions}
}
// writeOn implements [Condition].
func (o *or) writeOn(builder statementBuilder) {
if len(o.conditions) > 1 {
builder.writeString("(")
defer builder.writeString(")")
}
for i, condition := range o.conditions {
if i > 0 {
builder.writeString(" OR ")
}
condition.writeOn(builder)
}
}
var _ Condition = (*or)(nil)
type isNull struct {
column Column
}
func IsNull(column Column) *isNull {
return &isNull{column: column}
}
// writeOn implements [Condition].
func (cond *isNull) writeOn(builder statementBuilder) {
cond.column.Write(builder)
builder.writeString(" IS NULL")
}
var _ Condition = (*isNull)(nil)
type isNotNull struct {
column Column
}
func IsNotNull(column Column) *isNotNull {
return &isNotNull{column: column}
}
// writeOn implements [Condition].
func (cond *isNotNull) writeOn(builder statementBuilder) {
cond.column.Write(builder)
builder.writeString(" IS NOT NULL")
}
var _ Condition = (*isNotNull)(nil)
type condition[Op Operator, V Value] struct {
column Column
operator Op
value V
}
// writeOn implements [Condition].
func (cond condition[Op, V]) writeOn(builder statementBuilder) {
cond.column.Write(builder)
builder.writeString(cond.operator.String())
builder.writeString(builder.appendArg(cond.value))
}
var _ Condition = (*condition[TextOperator, string])(nil)
type textCondition[V Text] struct {
condition[TextOperator, V]
}
func NewTextCondition[V Text](column Column, operator TextOperator, value V) *textCondition[V] {
return &textCondition[V]{
condition: condition[TextOperator, V]{
column: column,
operator: operator,
value: value,
},
}
}
// writeOn implements [Condition].
func (cond *textCondition[V]) writeOn(builder statementBuilder) {
switch cond.operator {
case TextOperatorEqual, TextOperatorNotEqual:
cond.column.Write(builder)
builder.writeString(cond.operator.String())
builder.writeString(builder.appendArg(cond.value))
case TextOperatorEqualIgnoreCase, TextOperatorNotEqualIgnoreCase:
if col, ok := cond.column.(ignoreCaseColumn); ok {
col.WriteIgnoreCase(builder)
} else {
builder.writeString("LOWER(")
cond.column.Write(builder)
builder.writeString(")")
}
builder.writeString(cond.operator.String())
builder.writeString("LOWER(")
builder.writeString(builder.appendArg(cond.value))
builder.writeString(")")
case TextOperatorStartsWith:
cond.column.Write(builder)
builder.writeString(cond.operator.String())
builder.writeString(builder.appendArg(cond.value))
builder.writeString(" || '%'")
case TextOperatorStartsWithIgnoreCase:
if col, ok := cond.column.(ignoreCaseColumn); ok {
col.WriteIgnoreCase(builder)
} else {
builder.writeString("LOWER(")
cond.column.Write(builder)
builder.writeString(")")
}
builder.writeString(cond.operator.String())
builder.writeString("LOWER(")
builder.writeString(builder.appendArg(cond.value))
builder.writeString(") || '%'")
}
}
var _ Condition = (*textCondition[string])(nil)
type numberCondition[V Number] struct {
condition[NumberOperator, V]
}
func NewNumberCondition[V Number](column Column, operator NumberOperator, value V) *numberCondition[V] {
return &numberCondition[V]{
condition: condition[NumberOperator, V]{
column: column,
operator: operator,
value: value,
},
}
}
var _ Condition = (*numberCondition[int])(nil)

View File

@@ -1,104 +0,0 @@
package v3
import (
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Instance struct {
id string
name string
createdAt time.Time
updatedAt time.Time
deletedAt time.Time
}
// Columns implements [object].
func (Instance) Columns(table Table) []Column {
return []Column{
&column{
table: table,
name: columnNameID,
},
&column{
table: table,
name: columnNameName,
},
&column{
table: table,
name: columnNameCreatedAt,
},
&column{
table: table,
name: columnNameUpdatedAt,
},
&column{
table: table,
name: columnNameDeletedAt,
},
}
}
// Scan implements [object].
func (i Instance) Scan(row database.Scanner) error {
return row.Scan(
&i.id,
&i.name,
&i.createdAt,
&i.updatedAt,
&i.deletedAt,
)
}
type instanceTable struct {
*table
}
func InstanceTable() *instanceTable {
table := &instanceTable{
table: newTable[Instance]("zitadel", "instances"),
}
table.possibleJoins = func(t Table) map[string]Column {
switch on := t.(type) {
case *instanceTable:
return map[string]Column{
columnNameID: on.IDColumn(),
}
case *orgTable:
return map[string]Column{
columnNameID: on.InstanceIDColumn(),
}
case *userTable:
return map[string]Column{
columnNameID: on.InstanceIDColumn(),
}
default:
return nil
}
}
return table
}
func (i *instanceTable) IDColumn() Column {
return i.columns[columnNameID]
}
func (i *instanceTable) NameColumn() Column {
return i.columns[columnNameName]
}
func (i *instanceTable) CreatedAtColumn() Column {
return i.columns[columnNameCreatedAt]
}
func (i *instanceTable) UpdatedAtColumn() Column {
return i.columns[columnNameUpdatedAt]
}
func (i *instanceTable) DeletedAtColumn() Column {
return i.columns[columnNameDeletedAt]
}

View File

@@ -1,11 +0,0 @@
package v3
type join struct {
table Table
conditions []joinCondition
}
type joinCondition struct {
left Column
right Column
}

View File

@@ -1,82 +0,0 @@
package v3
import (
"fmt"
"time"
"golang.org/x/exp/constraints"
)
type Value interface {
Bool | Number | Text
}
type Text interface {
~string | ~[]byte
}
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration
}
type Bool interface {
~bool
}
type Operator interface {
fmt.Stringer
}
type TextOperator uint8
// String implements [Operator].
func (t TextOperator) String() string {
return textOperators[t]
}
const (
TextOperatorEqual TextOperator = iota + 1
TextOperatorEqualIgnoreCase
TextOperatorNotEqual
TextOperatorNotEqualIgnoreCase
TextOperatorStartsWith
TextOperatorStartsWithIgnoreCase
)
var textOperators = map[TextOperator]string{
TextOperatorEqual: " = ",
TextOperatorEqualIgnoreCase: " LIKE ",
TextOperatorNotEqual: " <> ",
TextOperatorNotEqualIgnoreCase: " NOT LIKE ",
TextOperatorStartsWith: " LIKE ",
TextOperatorStartsWithIgnoreCase: " LIKE ",
}
var _ Operator = TextOperator(0)
type NumberOperator uint8
// String implements Operator.
func (n NumberOperator) String() string {
return numberOperators[n]
}
const (
NumberOperatorEqual NumberOperator = iota + 1
NumberOperatorNotEqual
NumberOperatorLessThan
NumberOperatorLessThanOrEqual
NumberOperatorGreaterThan
NumberOperatorGreaterThanOrEqual
)
var numberOperators = map[NumberOperator]string{
NumberOperatorEqual: " = ",
NumberOperatorNotEqual: " <> ",
NumberOperatorLessThan: " < ",
NumberOperatorLessThanOrEqual: " <= ",
NumberOperatorGreaterThan: " > ",
NumberOperatorGreaterThanOrEqual: " >= ",
}
var _ Operator = NumberOperator(0)

View File

@@ -1,117 +0,0 @@
package v3
import (
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Org struct {
instanceID string
id string
name string
createdAt time.Time
updatedAt time.Time
deletedAt time.Time
}
// Columns implements [object].
func (Org) Columns(table Table) []Column {
return []Column{
&column{
table: table,
name: columnNameInstanceID,
},
&column{
table: table,
name: columnNameID,
},
&column{
table: table,
name: columnNameName,
},
&column{
table: table,
name: columnNameCreatedAt,
},
&column{
table: table,
name: columnNameUpdatedAt,
},
&column{
table: table,
name: columnNameDeletedAt,
},
}
}
// Scan implements [object].
func (o Org) Scan(row database.Scanner) error {
return row.Scan(
&o.instanceID,
&o.id,
&o.name,
&o.createdAt,
&o.updatedAt,
&o.deletedAt,
)
}
type orgTable struct {
*table
}
func OrgTable() *orgTable {
table := &orgTable{
table: newTable[Org]("zitadel", "orgs"),
}
table.possibleJoins = func(table Table) map[string]Column {
switch on := table.(type) {
case *instanceTable:
return map[string]Column{
columnNameInstanceID: on.IDColumn(),
}
case *orgTable:
return map[string]Column{
columnNameInstanceID: on.InstanceIDColumn(),
columnNameID: on.IDColumn(),
}
case *userTable:
return map[string]Column{
columnNameInstanceID: on.InstanceIDColumn(),
columnNameID: on.IDColumn(),
}
default:
return nil
}
}
return table
}
func (o *orgTable) InstanceIDColumn() Column {
return o.columns[columnNameInstanceID]
}
func (o *orgTable) IDColumn() Column {
return o.columns[columnNameID]
}
func (o *orgTable) NameColumn() Column {
return o.columns[columnNameName]
}
func (o *orgTable) CreatedAtColumn() Column {
return o.columns[columnNameCreatedAt]
}
func (o *orgTable) UpdatedAtColumn() Column {
return o.columns[columnNameUpdatedAt]
}
func (o *orgTable) DeletedAtColumn() Column {
return o.columns[columnNameDeletedAt]
}

View File

@@ -1,188 +0,0 @@
package v3
import (
"context"
"fmt"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type Query[O object] interface {
Where(condition Condition)
Join(tables ...Table)
Limit(limit uint32)
Offset(offset uint32)
OrderBy(columns ...Column)
Result(ctx context.Context, client database.Querier) (*O, error)
Results(ctx context.Context, client database.Querier) ([]O, error)
fmt.Stringer
statementBuilder
}
type query[O object] struct {
*statement[O]
joins []join
limit uint32
offset uint32
orderBy []Column
}
func NewQuery[O object](table Table) Query[O] {
return &query[O]{
statement: newStatement[O](table),
}
}
// Result implements [Query].
func (q *query[O]) Result(ctx context.Context, client database.Querier) (*O, error) {
var object O
row := client.QueryRow(ctx, q.String(), q.args...)
if err := object.Scan(row); err != nil {
return nil, err
}
return &object, nil
}
// Results implements [Query].
func (q *query[O]) Results(ctx context.Context, client database.Querier) ([]O, error) {
var objects []O
rows, err := client.Query(ctx, q.String(), q.args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var object O
if err := object.Scan(rows); err != nil {
return nil, err
}
objects = append(objects, object)
}
return objects, rows.Err()
}
// Join implements [Query].
func (q *query[O]) Join(tables ...Table) {
for _, tbl := range tables {
cols := q.tbl.(*table).possibleJoins(tbl)
if len(cols) == 0 {
panic(fmt.Sprintf("table %q does not have any possible joins with table %q", q.tbl.Name(), tbl.Name()))
}
q.joins = append(q.joins, join{
table: tbl,
conditions: make([]joinCondition, 0, len(cols)),
})
for colName, col := range cols {
q.joins[len(q.joins)-1].conditions = append(q.joins[len(q.joins)-1].conditions, joinCondition{
left: q.tbl.(*table).columns[colName],
right: col,
})
}
}
}
func (q *query[O]) Limit(limit uint32) {
q.limit = limit
}
func (q *query[O]) Offset(offset uint32) {
q.offset = offset
}
func (q *query[O]) OrderBy(columns ...Column) {
for _, allowedColumn := range q.columns {
for _, column := range columns {
if allowedColumn.Name() == column.Name() {
q.orderBy = append(q.orderBy, column)
}
}
}
}
// String implements [fmt.Stringer] and [Query].
func (q *query[O]) String() string {
q.writeSelectColumns()
q.writeFrom()
q.writeJoins()
q.writeCondition()
q.writeOrderBy()
q.writeLimit()
q.writeOffset()
q.writeGroupBy()
return q.builder.String()
}
func (q *query[O]) writeSelectColumns() {
q.builder.WriteString("SELECT ")
for i, column := range q.columns {
if i > 0 {
q.builder.WriteString(", ")
}
q.builder.WriteString(q.tbl.Alias())
q.builder.WriteRune('.')
q.builder.WriteString(column.Name())
}
}
func (q *query[O]) writeJoins() {
for _, join := range q.joins {
q.builder.WriteString(" JOIN ")
q.builder.WriteString(join.table.Schema())
q.builder.WriteRune('.')
q.builder.WriteString(join.table.Name())
if join.table.Alias() != "" {
q.builder.WriteString(" AS ")
q.builder.WriteString(join.table.Alias())
}
q.builder.WriteString(" ON ")
for i, condition := range join.conditions {
if i > 0 {
q.builder.WriteString(" AND ")
}
q.builder.WriteString(condition.left.Name())
q.builder.WriteString(" = ")
q.builder.WriteString(condition.right.Name())
}
}
}
func (q *query[O]) writeOrderBy() {
if len(q.orderBy) == 0 {
return
}
q.builder.WriteString(" ORDER BY ")
for i, order := range q.orderBy {
if i > 0 {
q.builder.WriteString(", ")
}
order.Write(q)
}
}
func (q *query[O]) writeLimit() {
if q.limit == 0 {
return
}
q.builder.WriteString(" LIMIT ")
q.builder.WriteString(q.appendArg(q.limit))
}
func (q *query[O]) writeOffset() {
if q.offset == 0 {
return
}
q.builder.WriteString(" OFFSET ")
q.builder.WriteString(q.appendArg(q.offset))
}
func (q *query[O]) writeGroupBy() {
q.builder.WriteString(" GROUP BY ")
}

View File

@@ -1,85 +0,0 @@
package v3
import (
"fmt"
"strings"
)
type statement[T object] struct {
tbl Table
columns []Column
condition Condition
builder strings.Builder
args []any
existingArgs map[any]string
}
func newStatement[O object](t Table) *statement[O] {
var o O
return &statement[O]{
tbl: t,
columns: o.Columns(t),
}
}
// Where implements [Query].
func (stmt *statement[T]) Where(condition Condition) {
stmt.condition = condition
}
func (stmt *statement[T]) writeFrom() {
stmt.builder.WriteString(" FROM ")
stmt.builder.WriteString(stmt.tbl.Schema())
stmt.builder.WriteRune('.')
stmt.builder.WriteString(stmt.tbl.Name())
if stmt.tbl.Alias() != "" {
stmt.builder.WriteString(" AS ")
stmt.builder.WriteString(stmt.tbl.Alias())
}
}
func (stmt *statement[T]) writeCondition() {
if stmt.condition == nil {
return
}
stmt.builder.WriteString(" WHERE ")
stmt.condition.writeOn(stmt)
}
// appendArg implements [statementBuilder].
func (stmt *statement[T]) appendArg(arg any) (placeholder string) {
if stmt.existingArgs == nil {
stmt.existingArgs = make(map[any]string)
}
if placeholder, ok := stmt.existingArgs[arg]; ok {
return placeholder
}
stmt.args = append(stmt.args, arg)
placeholder = fmt.Sprintf("$%d", len(stmt.args))
stmt.existingArgs[arg] = placeholder
return placeholder
}
// table implements [statementBuilder].
func (stmt *statement[T]) table() Table {
return stmt.tbl
}
// write implements [statementBuilder].
func (stmt *statement[T]) write(data []byte) {
stmt.builder.Write(data)
}
// writeRune implements [statementBuilder].
func (stmt *statement[T]) writeRune(r rune) {
stmt.builder.WriteRune(r)
}
// writeString implements [statementBuilder].
func (stmt *statement[T]) writeString(s string) {
stmt.builder.WriteString(s)
}
var _ statementBuilder = (*statement[Instance])(nil)

View File

@@ -1,84 +0,0 @@
package v3
import "github.com/zitadel/zitadel/backend/v3/storage/database"
type object interface {
User | Org | Instance
Columns(t Table) []Column
Scan(s database.Scanner) error
}
type Table interface {
Schema() string
Name() string
Alias() string
Columns() []Column
writeOn(builder statementBuilder)
}
type table struct {
schema string
name string
alias string
possibleJoins func(table Table) map[string]Column
columns map[string]Column
colList []Column
}
func newTable[O object](schema, name string) *table {
t := &table{
schema: schema,
name: name,
}
var o O
t.colList = o.Columns(t)
t.columns = make(map[string]Column, len(t.colList))
for _, col := range t.colList {
t.columns[col.Name()] = col
}
return t
}
// Columns implements [Table].
func (t *table) Columns() []Column {
if len(t.colList) > 0 {
return t.colList
}
t.colList = make([]Column, 0, len(t.columns))
for _, column := range t.columns {
t.colList = append(t.colList, column)
}
return t.colList
}
// Name implements [Table].
func (t *table) Name() string {
return t.name
}
// Schema implements [Table].
func (t *table) Schema() string {
return t.schema
}
// Alias implements [Table].
func (t *table) Alias() string {
if t.alias != "" {
return t.alias
}
return t.schema + "." + t.name
}
// writeOn implements [Table].
func (t *table) writeOn(builder statementBuilder) {
builder.writeString(t.Alias())
}
var _ Table = (*table)(nil)

View File

@@ -1,170 +0,0 @@
package v3
import (
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type User struct {
instanceID string
orgID string
id string
username string
createdAt time.Time
updatedAt time.Time
deletedAt time.Time
}
// Columns implements [object].
func (u User) Columns(table Table) []Column {
return []Column{
&column{
table: table,
name: columnNameInstanceID,
},
&column{
table: table,
name: columnNameOrgID,
},
&column{
table: table,
name: columnNameID,
},
&columnIgnoreCase{
column: column{
table: table,
name: userTableUsernameColumn,
},
suffix: "_lower",
},
&column{
table: table,
name: columnNameCreatedAt,
},
&column{
table: table,
name: columnNameUpdatedAt,
},
&column{
table: table,
name: columnNameDeletedAt,
},
}
}
// Scan implements [object].
func (u User) Scan(row database.Scanner) error {
return row.Scan(
&u.instanceID,
&u.orgID,
&u.id,
&u.username,
&u.createdAt,
&u.updatedAt,
&u.deletedAt,
)
}
type userTable struct {
*table
}
const (
userTableUsernameColumn = "username"
)
func UserTable() *userTable {
table := &userTable{
table: newTable[User]("zitadel", "users"),
}
table.possibleJoins = func(table Table) map[string]Column {
switch on := table.(type) {
case *userTable:
return map[string]Column{
columnNameInstanceID: on.InstanceIDColumn(),
columnNameOrgID: on.OrgIDColumn(),
columnNameID: on.IDColumn(),
}
case *orgTable:
return map[string]Column{
columnNameInstanceID: on.InstanceIDColumn(),
columnNameOrgID: on.IDColumn(),
}
case *instanceTable:
return map[string]Column{
columnNameInstanceID: on.IDColumn(),
}
default:
return nil
}
}
return table
}
func (t *userTable) InstanceIDColumn() Column {
return t.columns[columnNameInstanceID]
}
func (t *userTable) OrgIDColumn() Column {
return t.columns[columnNameOrgID]
}
func (t *userTable) IDColumn() Column {
return t.columns[columnNameID]
}
func (t *userTable) UsernameColumn() Column {
return t.columns[userTableUsernameColumn]
}
func (t *userTable) CreatedAtColumn() Column {
return t.columns[columnNameCreatedAt]
}
func (t *userTable) UpdatedAtColumn() Column {
return t.columns[columnNameUpdatedAt]
}
func (t *userTable) DeletedAtColumn() Column {
return t.columns[columnNameDeletedAt]
}
func NewUserQuery() Query[User] {
q := NewQuery[User](UserTable())
return q
}
type userByIDCondition[T Text] struct {
id T
}
func UserByID[T Text](id T) Condition {
return &userByIDCondition[T]{id: id}
}
// writeOn implements Condition.
func (u *userByIDCondition[T]) writeOn(builder statementBuilder) {
NewTextCondition(builder.table().(*userTable).IDColumn(), TextOperatorEqual, u.id).writeOn(builder)
}
var _ Condition = (*userByIDCondition[string])(nil)
type userByUsernameCondition[T Text] struct {
username T
operator TextOperator
}
func UserByUsername[T Text](username T, operator TextOperator) Condition {
return &userByUsernameCondition[T]{username: username, operator: operator}
}
// writeOn implements Condition.
func (u *userByUsernameCondition[T]) writeOn(builder statementBuilder) {
NewTextCondition(builder.table().(*userTable).UsernameColumn(), u.operator, u.username).writeOn(builder)
}
var _ Condition = (*userByUsernameCondition[string])(nil)

View File

@@ -1,25 +0,0 @@
package v3_test
import (
"context"
"testing"
v3 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v3"
)
type user struct{}
func TestUser(t *testing.T) {
query := v3.NewUserQuery()
query.Where(
v3.Or(
v3.UserByID("123"),
v3.UserByUsername("test", v3.TextOperatorStartsWithIgnoreCase),
),
)
query.Limit(10)
query.Offset(5)
// query.OrderBy(
query.Result(context.TODO(), nil)
}

View File

@@ -1,2 +0,0 @@
// this test focuses on queries rather than on tables
package v4

View File

@@ -1,17 +0,0 @@
package v4
type Org struct {
InstanceID string
ID string
Name string
}
type GetOrg struct{}
type ListOrgs struct{}
type CreateOrg struct{}
type UpdateOrg struct{}
type DeleteOrg struct{}

View File

@@ -1,284 +0,0 @@
package v4
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` +
` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
` FROM users_view`
type user struct {
builder database.StatementBuilder
client database.QueryExecutor
}
func UserRepository(client database.QueryExecutor) domain.UserRepository {
return &user{
client: client,
}
}
var _ domain.UserRepository = (*user)(nil)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
// Human implements [domain.UserRepository].
func (u *user) Human() domain.HumanRepository {
return &userHuman{user: u}
}
// Machine implements [domain.UserRepository].
func (u *user) Machine() domain.MachineRepository {
return &userMachine{user: u}
}
// List implements [domain.UserRepository].
func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
rows, err := u.client.Query(ctx, u.builder.String(), u.builder.Args()...)
if err != nil {
return nil, err
}
defer func() {
closeErr := rows.Close()
if err != nil {
return
}
err = closeErr
}()
for rows.Next() {
user, err := scanUser(rows)
if err != nil {
return nil, err
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
// Get implements [domain.UserRepository].
func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...))
}
const (
createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` +
` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` +
` RETURNING created_at, updated_at`
createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` +
` VALUES ($1, $2, $3, $4, $5)` +
` RETURNING created_at, updated_at`
)
// Create implements [domain.UserRepository].
func (u *user) Create(ctx context.Context, user *domain.User) error {
u.builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type())
switch trait := user.Traits.(type) {
case *domain.Human:
u.builder.WriteString(createHumanStmt)
u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
case *domain.Machine:
u.builder.WriteString(createMachineStmt)
u.builder.AppendArgs(trait.Description)
}
return u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
}
// Delete implements [domain.UserRepository].
func (u *user) Delete(ctx context.Context, condition database.Condition) error {
u.builder.WriteString("DELETE FROM users")
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetUsername implements [domain.userChanges].
func (u user) SetUsername(username string) database.Change {
return database.NewChange(u.UsernameColumn(), username)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// InstanceIDCondition implements [domain.userConditions].
func (u user) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// OrgIDCondition implements [domain.userConditions].
func (u user) OrgIDCondition(orgID string) database.Condition {
return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID)
}
// IDCondition implements [domain.userConditions].
func (u user) IDCondition(userID string) database.Condition {
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
}
// UsernameCondition implements [domain.userConditions].
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
return database.NewTextCondition(u.UsernameColumn(), op, username)
}
// CreatedAtCondition implements [domain.userConditions].
func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt)
}
// UpdatedAtCondition implements [domain.userConditions].
func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
}
// DeletedCondition implements [domain.userConditions].
func (u user) DeletedCondition(isDeleted bool) database.Condition {
if isDeleted {
return database.IsNotNull(u.DeletedAtColumn())
}
return database.IsNull(u.DeletedAtColumn())
}
// DeletedAtCondition implements [domain.userConditions].
func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition {
return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// InstanceIDColumn implements [domain.userColumns].
func (user) InstanceIDColumn() database.Column {
return database.NewColumn("instance_id")
}
// OrgIDColumn implements [domain.userColumns].
func (user) OrgIDColumn() database.Column {
return database.NewColumn("org_id")
}
// IDColumn implements [domain.userColumns].
func (user) IDColumn() database.Column {
return database.NewColumn("id")
}
// UsernameColumn implements [domain.userColumns].
func (user) UsernameColumn() database.Column {
return database.NewIgnoreCaseColumn("username", "_lower")
}
// FirstNameColumn implements [domain.userColumns].
func (user) CreatedAtColumn() database.Column {
return database.NewColumn("created_at")
}
// UpdatedAtColumn implements [domain.userColumns].
func (user) UpdatedAtColumn() database.Column {
return database.NewColumn("updated_at")
}
// DeletedAtColumn implements [domain.userColumns].
func (user) DeletedAtColumn() database.Column {
return database.NewColumn("deleted_at")
}
func (u *user) writeCondition(condition database.Condition) {
if condition == nil {
return
}
u.builder.WriteString(" WHERE ")
condition.Write(&u.builder)
}
func (u user) columns() database.Columns {
return database.Columns{
u.InstanceIDColumn(),
u.OrgIDColumn(),
u.IDColumn(),
u.UsernameColumn(),
u.CreatedAtColumn(),
u.UpdatedAtColumn(),
u.DeletedAtColumn(),
}
}
func scanUser(scanner database.Scanner) (*domain.User, error) {
var (
user domain.User
human domain.Human
email domain.Email
phone domain.Phone
machine domain.Machine
typ domain.UserType
)
err := scanner.Scan(
&user.InstanceID,
&user.OrgID,
&user.ID,
&user.Username,
&typ,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
&human.FirstName,
&human.LastName,
&email.Address,
&email.VerifiedAt,
&phone.Number,
&phone.VerifiedAt,
&machine.Description,
)
if err != nil {
return nil, err
}
switch typ {
case domain.UserTypeHuman:
if email.Address != "" {
human.Email = &email
}
if phone.Number != "" {
human.Phone = &phone
}
user.Traits = &human
case domain.UserTypeMachine:
user.Traits = &machine
}
return &user, nil
}

View File

@@ -1,39 +1,285 @@
package repository
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` +
` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
` FROM users_view`
type user struct {
database.QueryExecutor
repository
}
func User(client database.QueryExecutor) domain.UserRepository {
// return &user{QueryExecutor: client}
return nil
}
// On implements [domain.UserRepository].
func (exec *user) On(clauses ...domain.UserClause) domain.UserOperation {
return &userOperation{
QueryExecutor: exec.QueryExecutor,
clauses: clauses,
func UserRepository(client database.QueryExecutor) domain.UserRepository {
return &user{
repository: repository{
client: client,
},
}
}
// OnHuman implements [domain.UserRepository].
func (exec *user) OnHuman(clauses ...domain.UserClause) domain.HumanOperation {
return &humanOperation{
userOperation: *exec.On(clauses...).(*userOperation),
var _ domain.UserRepository = (*user)(nil)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
// Human implements [domain.UserRepository].
func (u *user) Human() domain.HumanRepository {
return &userHuman{user: u}
}
// Machine implements [domain.UserRepository].
func (u *user) Machine() domain.MachineRepository {
return &userMachine{user: u}
}
// List implements [domain.UserRepository].
func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
rows, err := u.client.Query(ctx, u.builder.String(), u.builder.Args()...)
if err != nil {
return nil, err
}
defer func() {
closeErr := rows.Close()
if err != nil {
return
}
err = closeErr
}()
for rows.Next() {
user, err := scanUser(rows)
if err != nil {
return nil, err
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
// Get implements [domain.UserRepository].
func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.WriteCondition(&u.builder)
options.WriteOrderBy(&u.builder)
options.WriteLimit(&u.builder)
options.WriteOffset(&u.builder)
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...))
}
const (
createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` +
` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` +
` RETURNING created_at, updated_at`
createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` +
` VALUES ($1, $2, $3, $4, $5)` +
` RETURNING created_at, updated_at`
)
// Create implements [domain.UserRepository].
func (u *user) Create(ctx context.Context, user *domain.User) error {
u.builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type())
switch trait := user.Traits.(type) {
case *domain.Human:
u.builder.WriteString(createHumanStmt)
u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
case *domain.Machine:
u.builder.WriteString(createMachineStmt)
u.builder.AppendArgs(trait.Description)
}
return u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
}
// Delete implements [domain.UserRepository].
func (u *user) Delete(ctx context.Context, condition database.Condition) error {
u.builder.WriteString("DELETE FROM users")
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetUsername implements [domain.userChanges].
func (u user) SetUsername(username string) database.Change {
return database.NewChange(u.UsernameColumn(), username)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// InstanceIDCondition implements [domain.userConditions].
func (u user) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// OrgIDCondition implements [domain.userConditions].
func (u user) OrgIDCondition(orgID string) database.Condition {
return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID)
}
// IDCondition implements [domain.userConditions].
func (u user) IDCondition(userID string) database.Condition {
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
}
// UsernameCondition implements [domain.userConditions].
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
return database.NewTextCondition(u.UsernameColumn(), op, username)
}
// CreatedAtCondition implements [domain.userConditions].
func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt)
}
// UpdatedAtCondition implements [domain.userConditions].
func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
}
// DeletedCondition implements [domain.userConditions].
func (u user) DeletedCondition(isDeleted bool) database.Condition {
if isDeleted {
return database.IsNotNull(u.DeletedAtColumn())
}
return database.IsNull(u.DeletedAtColumn())
}
// DeletedAtCondition implements [domain.userConditions].
func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition {
return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// InstanceIDColumn implements [domain.userColumns].
func (user) InstanceIDColumn() database.Column {
return database.NewColumn("instance_id")
}
// OrgIDColumn implements [domain.userColumns].
func (user) OrgIDColumn() database.Column {
return database.NewColumn("org_id")
}
// IDColumn implements [domain.userColumns].
func (user) IDColumn() database.Column {
return database.NewColumn("id")
}
// UsernameColumn implements [domain.userColumns].
func (user) UsernameColumn() database.Column {
return database.NewIgnoreCaseColumn("username", "_lower")
}
// FirstNameColumn implements [domain.userColumns].
func (user) CreatedAtColumn() database.Column {
return database.NewColumn("created_at")
}
// UpdatedAtColumn implements [domain.userColumns].
func (user) UpdatedAtColumn() database.Column {
return database.NewColumn("updated_at")
}
// DeletedAtColumn implements [domain.userColumns].
func (user) DeletedAtColumn() database.Column {
return database.NewColumn("deleted_at")
}
func (u *user) writeCondition(condition database.Condition) {
if condition == nil {
return
}
u.builder.WriteString(" WHERE ")
condition.Write(&u.builder)
}
func (u user) columns() database.Columns {
return database.Columns{
u.InstanceIDColumn(),
u.OrgIDColumn(),
u.IDColumn(),
u.UsernameColumn(),
u.CreatedAtColumn(),
u.UpdatedAtColumn(),
u.DeletedAtColumn(),
}
}
// OnMachine implements [domain.UserRepository].
func (exec *user) OnMachine(clauses ...domain.UserClause) domain.MachineOperation {
return &machineOperation{
userOperation: *exec.On(clauses...).(*userOperation),
func scanUser(scanner database.Scanner) (*domain.User, error) {
var (
user domain.User
human domain.Human
email domain.Email
phone domain.Phone
machine domain.Machine
typ domain.UserType
)
err := scanner.Scan(
&user.InstanceID,
&user.OrgID,
&user.ID,
&user.Username,
&typ,
&user.CreatedAt,
&user.UpdatedAt,
&user.DeletedAt,
&human.FirstName,
&human.LastName,
&email.Address,
&email.VerifiedAt,
&phone.Number,
&phone.VerifiedAt,
&machine.Description,
)
if err != nil {
return nil, err
}
}
// var _ domain.UserRepository = (*user)(nil)
switch typ {
case domain.UserTypeHuman:
if email.Address != "" {
human.Email = &email
}
if phone.Number != "" {
human.Phone = &phone
}
user.Traits = &human
case domain.UserTypeMachine:
user.Traits = &machine
}
return &user, nil
}

View File

@@ -1,4 +1,4 @@
package v4
package repository
import (
"context"

View File

@@ -1,36 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
)
type humanOperation struct {
userOperation
}
// GetEmail implements domain.HumanOperation.
func (h *humanOperation) GetEmail(ctx context.Context) (*domain.Email, error) {
var email domain.Email
err := h.QueryExecutor.QueryRow(ctx, `SELECT email, is_email_verified FROM human_users WHERE id = $1`, h.clauses).Scan(
&email.Address,
&email.IsVerified,
)
if err != nil {
return nil, err
}
return &email, nil
}
// SetEmail implements domain.HumanOperation.
func (h *humanOperation) SetEmail(ctx context.Context, email string) error {
return h.QueryExecutor.Exec(ctx, `UPDATE human_users SET email = $1 WHERE id = $2`, email, h.clauses)
}
// SetEmailVerified implements domain.HumanOperation.
func (h *humanOperation) SetEmailVerified(ctx context.Context, email string) error {
return h.QueryExecutor.Exec(ctx, `UPDATE human_users SET is_email_verified = $1 WHERE id = $2 AND email = $3`, true, h.clauses, email)
}
var _ domain.HumanOperation = (*humanOperation)(nil)

View File

@@ -1,18 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
)
type machineOperation struct {
userOperation
}
// SetDescription implements domain.MachineOperation.
func (m *machineOperation) SetDescription(ctx context.Context, description string) error {
return m.QueryExecutor.Exec(ctx, `UPDATE machines SET description = $1 WHERE id = $2`, description, m.clauses)
}
var _ domain.MachineOperation = (*machineOperation)(nil)

View File

@@ -1,68 +0,0 @@
package repository
import (
"context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type userOperation struct {
database.QueryExecutor
clauses []domain.UserClause
}
// Delete implements [domain.UserOperation].
func (u *userOperation) Delete(ctx context.Context) error {
return u.QueryExecutor.Exec(ctx, `DELETE FROM users WHERE id = $1`, u.clauses)
}
// SetUsername implements [domain.UserOperation].
func (u *userOperation) SetUsername(ctx context.Context, username string) error {
var stmt statement
stmt.builder.WriteString(`UPDATE users SET username = $1 WHERE `)
stmt.appendArg(username)
clausesToSQL(&stmt, u.clauses)
return u.QueryExecutor.Exec(ctx, stmt.builder.String(), stmt.args...)
}
var _ domain.UserOperation = (*userOperation)(nil)
func UserIDQuery(id string) domain.UserClause {
return textClause[string]{
clause: clause[database.TextOperation]{
field: userFields[domain.UserFieldID],
op: database.TextOperationEqual,
},
value: id,
}
}
func HumanEmailQuery(op database.TextOperation, email string) domain.UserClause {
return textClause[string]{
clause: clause[database.TextOperation]{
field: userFields[domain.UserHumanFieldEmail],
op: op,
},
value: email,
}
}
func HumanEmailVerifiedQuery(op database.BoolOperation) domain.UserClause {
return boolClause[database.BoolOperation]{
clause: clause[database.BoolOperation]{
field: userFields[domain.UserHumanFieldEmailVerified],
op: op,
},
}
}
func clausesToSQL(stmt *statement, clauses []domain.UserClause) {
for _, clause := range clauses {
stmt.builder.WriteString(userFields[clause.Field()].String())
stmt.builder.WriteString(clause.Operation().String())
stmt.appendArg(clause.Args()...)
}
}

View File

@@ -1,4 +1,4 @@
package v4_test
package repository_test
import (
"context"
@@ -6,12 +6,16 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/backend/v3/storage/database"
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"go.uber.org/mock/gomock"
)
func TestQueryUser(t *testing.T) {
t.Run("User filters", func(t *testing.T) {
user := v4.UserRepository(nil)
client := dbmock.NewMockClient(gomock.NewController(t))
user := repository.UserRepository(client)
u, err := user.Get(context.Background(),
database.WithCondition(
database.And(
@@ -30,7 +34,9 @@ func TestQueryUser(t *testing.T) {
})
t.Run("machine and human filters", func(t *testing.T) {
user := v4.UserRepository(nil)
client := dbmock.NewMockClient(gomock.NewController(t))
user := repository.UserRepository(client)
machine := user.Machine()
human := user.Human()
email, err := human.GetEmail(context.Background(), database.And(
@@ -62,7 +68,7 @@ func TestArg(t *testing.T) {
func TestWriteUser(t *testing.T) {
t.Run("update user", func(t *testing.T) {
user := v4.UserRepository(nil)
user := repository.UserRepository(nil)
user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
})
}

View File

@@ -12,16 +12,19 @@ const (
NullInstruction Instruction = "NULL"
)
// StatementBuilder is a helper to build SQL statement.
type StatementBuilder struct {
strings.Builder
args []any
existingArgs map[any]string
}
// WriteArgs adds the argument to the statement and writes the placeholder to the query.
func (b *StatementBuilder) WriteArg(arg any) {
b.WriteString(b.AppendArg(arg))
}
// AppebdArg adds the argument to the statement and returns the placeholder.
func (b *StatementBuilder) AppendArg(arg any) (placeholder string) {
if b.existingArgs == nil {
b.existingArgs = make(map[any]string)
@@ -39,12 +42,14 @@ func (b *StatementBuilder) AppendArg(arg any) (placeholder string) {
return placeholder
}
// AppendArgs adds the arguments to the statement and doesn't return the placeholders.
func (b *StatementBuilder) AppendArgs(args ...any) {
for _, arg := range args {
b.AppendArg(arg)
}
}
// Args returns the arguments added to the statement.
func (b *StatementBuilder) Args() []any {
return b.args
}