triggers and backend

This commit is contained in:
adlerhurst
2025-01-06 08:00:35 +01:00
parent 2bfdb72bf3
commit 10acecb7a1
37 changed files with 1390 additions and 0 deletions

2
backend/internal/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// package contains domain logic of Zitadel
package internal

View File

@@ -0,0 +1,31 @@
package port
// type InstanceRepository interface {
// // CreateInstance creates a new instance
// CreateInstance(instance *domain.Instance) error
// // GetInstance returns the instance with the given id
// GetInstance(id string) (*domain.Instance, error)
// // UpdateInstance updates the instance with the given id
// UpdateInstance(instance *domain.Instance) error
// // DeleteInstance deletes the instance with the given id
// DeleteInstance(id string) error
// }
// type InstanceDomainRepository interface {
// // CreateDomain creates a new domain for the instance
// CreateDomain(instanceID string, domain *domain.Domain) error
// // GetDomains returns the domains of an instance
// GetDomains(instanceID string) ([]*domain.Domain, error)
// // UpdateDomain updates the domain of an instance
// UpdateDomain(instanceID string, domain *domain.Domain) error
// // DeleteDomain deletes the domain of an instance
// DeleteDomain(instanceID, domain string) error
// }
// type DomainGenerator interface {
// GenerateDomain() (string, error)
// }
// type IDGenerator interface {
// GenerateID() (string, error)
// }

View File

@@ -0,0 +1,75 @@
package port
import "context"
type Operation uint8
const (
OperationEqual Operation = iota
)
type Object interface {
Columns() []*Column
}
type Column struct {
Name string
Value any
}
type Filter interface {
Column() *Column
Operation() Operation
}
var _ Filter = (*filter)(nil)
type filter struct {
column *Column
op Operation
}
func newFilter(column *Column, op Operation) Filter {
return &filter{column: column, op: op}
}
func (f *filter) Column() *Column {
return f.column
}
func (f *filter) Operation() Operation {
return f.op
}
func NewEqualFilter(column *Column) Filter {
return newFilter(column, OperationEqual)
}
type Querier[T any] interface {
Get(ctx context.Context, filters []Filter) (T, error)
List(ctx context.Context, filters []Filter) ([]T, error)
}
type Executor[T Object] interface {
Create(ctx context.Context, object T) error
Update(ctx context.Context, columns []*Column, filters []Filter) error
Delete(ctx context.Context, filters []Filter) error
}
type Pool[T Object] interface {
Acquire(ctx context.Context) (Client[T], error)
Begin(ctx context.Context) (Transaction[T], error)
}
type Client[T Object] interface {
Querier[T]
Executor[T]
Begin(ctx context.Context) (Transaction[T], error)
Release(ctx context.Context) error
}
type Transaction[T Object] interface {
Executor[T]
Querier[T]
End(ctx context.Context, gotErr error) error
}

View File

@@ -0,0 +1,352 @@
package consistent
import (
"context"
"errors"
"reflect"
"slices"
"strings"
"sync"
"github.com/zitadel/zitadel/backend/internal/port"
"github.com/zitadel/zitadel/backend/internal/port/storage"
)
var (
_ port.Executor[port.Object] = (*MapRepository[port.Object])(nil)
_ port.Querier[port.Object] = (*MapRepository[port.Object])(nil)
_ port.Client[port.Object] = (*MapRepository[port.Object])(nil)
// _ port.Pool[any] = (*MapRepository[any])(nil)
_ port.Executor[port.Object] = (*MapTransaction[port.Object])(nil)
_ port.Querier[port.Object] = (*MapTransaction[port.Object])(nil)
_ port.Transaction[port.Object] = (*MapTransaction[port.Object])(nil)
)
var pkFields = &sync.Map{}
type MapRepository[T port.Object] struct {
objects []*row[T]
}
type row[T port.Object] struct {
object *T
mu *sync.RWMutex
}
func (m *MapRepository[T]) Begin(ctx context.Context) (port.Transaction[T], error) {
return &MapTransaction[T]{
repo: m,
}, nil
}
func (m *MapRepository[T]) Release(ctx context.Context) error {
return nil
}
// Create implements [port.Executor]
func (m *MapRepository[T]) Create(ctx context.Context, object T) (err error) {
tx, err := m.Begin(ctx)
if err != nil {
return err
}
defer tx.End(ctx, err)
return tx.Create(ctx, object)
}
// Delete implements [port.Executor]
func (m *MapRepository[T]) Delete(ctx context.Context, filters []port.Filter) (err error) {
tx, err := m.Begin(ctx)
if err != nil {
return err
}
defer tx.End(ctx, err)
return tx.Delete(ctx, filters)
}
// Update implements [port.Executor]
func (m *MapRepository[T]) Update(ctx context.Context, columns []*port.Column, filters []port.Filter) (err error) {
tx, err := m.Begin(ctx)
if err != nil {
return err
}
defer tx.End(ctx, err)
return tx.Update(ctx, columns, filters)
}
// Get implements [port.Querier]
func (m *MapRepository[T]) Get(ctx context.Context, filters []port.Filter) (o T, err error) {
tx, err := m.Begin(ctx)
if err != nil {
return o, err
}
defer tx.End(ctx, err)
return tx.Get(ctx, filters)
}
// List implements [port.Querier]
func (m *MapRepository[T]) List(ctx context.Context, filters []port.Filter) (_ []T, err error) {
tx, err := m.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.End(ctx, err)
return tx.List(ctx, filters)
}
type MapTransaction[T port.Object] struct {
repo *MapRepository[T]
changes []*change[T]
}
type change[T port.Object] struct {
row *row[T]
changed *T
typ changeType
}
type changeType uint8
const (
createChangeType changeType = iota
updateChangeType
deleteChangeType
)
func (m *MapTransaction[T]) End(ctx context.Context, gotErr error) error {
if ctx.Err() == nil && gotErr == nil {
return m.commit()
}
return m.rollback()
}
func (m *MapTransaction[T]) commit() error {
for _, change := range m.changes {
switch change.typ {
case createChangeType:
change.row.object = change.changed
case deleteChangeType:
m.repo.objects = slices.DeleteFunc(m.repo.objects, func(r *row[T]) bool {
return r == change.row
})
change.row.mu.Unlock()
change.row = nil
continue
case updateChangeType:
*change.row.object = *change.changed
default:
return errors.New("unknown change type")
}
change.row.mu.Unlock()
}
return nil
}
func (m *MapTransaction[T]) rollback() error {
for _, change := range m.changes {
if change.changed == nil {
// TODO: remove from repo.objects
}
change.row.mu.Unlock()
change = nil
}
m.changes = nil
return nil
}
// Create implements port.Executor.
func (m *MapTransaction[T]) Create(ctx context.Context, object T) error {
_, err := m.get(ctx, pkFilter(object))
if err == nil {
return storage.ErrAlreadyExists
}
mu := &sync.RWMutex{}
mu.Lock()
m.repo.objects = append(m.repo.objects, &row[T]{
object: &object,
mu: mu,
})
m.changes = append(m.changes, &change[T]{
row: m.repo.objects[len(m.repo.objects)-1],
typ: createChangeType,
})
return nil
}
// Delete implements port.Executor.
func (m *MapTransaction[T]) Delete(ctx context.Context, filters []port.Filter) error {
rows, err := m.list(ctx, filters)
if err != nil {
return err
}
for _, row := range rows {
row.mu.Lock()
m.changes = append(m.changes, &change[T]{
row: row,
typ: deleteChangeType,
})
}
return nil
}
// Update implements port.Executor.
func (m *MapTransaction[T]) Update(ctx context.Context, columns []*port.Column, filters []port.Filter) error {
rows, err := m.list(ctx, filters)
if err != nil {
return err
}
for _, row := range rows {
row.mu.Lock()
changed, err := update(row, columns)
if err != nil {
return err
}
m.changes = append(m.changes, &change[T]{
row: row,
changed: changed,
typ: updateChangeType,
})
}
return nil
}
// Get implements [port.Querier]
func (m *MapTransaction[T]) Get(ctx context.Context, filters []port.Filter) (o T, err error) {
r, err := m.get(ctx, filters)
if err != nil {
return o, err
}
r.mu.RLock()
return o, nil
}
func (m *MapTransaction[T]) get(ctx context.Context, filters []port.Filter) (o *row[T], err error) {
rows, err := m.list(ctx, filters)
if err != nil {
return nil, err
}
if len(rows) > 1 {
return nil, errors.New("multiple rows returned")
}
return rows[0], nil
}
// List implements [port.Querier]
func (m *MapTransaction[T]) List(ctx context.Context, filters []port.Filter) ([]T, error) {
rows, err := m.list(ctx, filters)
if err != nil {
return nil, err
}
objects := make([]T, len(rows))
for i, row := range rows {
row.mu.RLock()
objects[i] = *row.object
}
return objects, nil
}
func (m *MapTransaction[T]) list(ctx context.Context, filters []port.Filter) (res []*row[T], err error) {
res = slices.Clone(m.repo.objects)
for _, filter := range filters {
res = slices.DeleteFunc(res, func(r *row[T]) bool {
v := reflect.ValueOf(r.object)
definition := getDefinition[T]()
return v.Field(definition.columnIndexes[filter.Column().Name]).Interface() != filter.Column().Value
})
res = slices.Clip(res)
}
return res, nil
}
func pkFilter[T port.Object](o T) []port.Filter {
definition := getDefinition[T]()
v := reflect.ValueOf(o)
filters := make([]port.Filter, len(definition.pkFieldIndexes))
for _, idx := range definition.pkFieldIndexes {
filters = append(filters, port.NewEqualFilter(
&port.Column{
Name: definition.columnNames[idx],
Value: v.Field(idx).Interface(),
},
))
}
return filters
}
type structDefinition struct {
columnNames []string
columnIndexes map[string]int
pkFieldIndexes []int
fkFieldIndexes []int
}
func getDefinition[T port.Object]() *structDefinition {
definition, ok := pkFields.Load(reflect.TypeFor[T]())
if !ok {
definition = registerPkFields[T]()
}
return definition.(*structDefinition)
}
func registerPkFields[T port.Object]() *structDefinition {
t := reflect.TypeFor[T]()
definition := &structDefinition{
columnIndexes: make(map[string]int, t.NumField()),
columnNames: make([]string, t.NumField()),
pkFieldIndexes: make([]int, 0, t.NumField()),
fkFieldIndexes: make([]int, 0, t.NumField()),
}
for i := 0; i < t.NumField(); i++ {
tags := t.Field(i).Tag.Get("consistent")
fields := strings.Split(tags, ",")
if fields[0] == "" {
fields[0] = t.Field(i).Name
}
definition.columnIndexes[fields[0]] = i
definition.columnNames[i] = fields[0]
for _, field := range fields[1:] {
switch field {
case "pk":
definition.pkFieldIndexes = append(definition.pkFieldIndexes, i)
case "fk":
definition.fkFieldIndexes = append(definition.fkFieldIndexes, i)
}
}
}
pkFields.Store(t, definition)
return definition
}
func update[T port.Object](r *row[T], columns []*port.Column) (o *T, err error) {
v := reflect.Zero(reflect.TypeOf(r.object))
current := reflect.ValueOf(r.object)
definition := getDefinition[T]()
fields:
for i := 0; i < v.NumField(); i++ {
for _, column := range columns {
if definition.columnIndexes[column.Name] != i {
continue
}
v.Field(i).Set(reflect.ValueOf(column.Value))
continue fields
}
v.Field(i).Set(current.Field(i))
}
return r.object, nil
}

View File

@@ -0,0 +1,35 @@
package consistent
import "github.com/zitadel/zitadel/backend/internal/port"
var _ port.Object = (*testInstance)(nil)
type testInstance struct {
ID string `consistent:"id,pk"`
Name string `consistent:"name,pk"`
Domains []*testDomain `consistent:"domains"`
}
func (i *testInstance) Columns() []*port.Column {
return []*port.Column{
{Name: "id", Value: i.ID},
{Name: "name", Value: i.Name},
{Name: "domains", Value: i.Domains},
}
}
var _ port.Object = (*testDomain)(nil)
type testDomain struct {
Name string `consistent:"name,pk"`
InstanceID string `consistent:"instance_id,pk,fk"`
IsVerified bool `consistent:"is_verified"`
}
func (d *testDomain) Columns() []*port.Column {
return []*port.Column{
{Name: "name", Value: d.Name},
{Name: "instance_id", Value: d.InstanceID},
{Name: "is_verified", Value: d.IsVerified},
}
}

View File

@@ -0,0 +1,7 @@
package storage
import "errors"
var (
ErrAlreadyExists = errors.New("already exists")
)

View File

@@ -0,0 +1,89 @@
package port
import (
"context"
"slices"
)
type Getter[T any] interface {
Get(ctx context.Context, filters []Filter) (T, error)
}
func Get[T Object](ctx context.Context, get func(ctx context.Context, filters []Filter) (T, error), filters []Filter) (T, error) {
return get(ctx, filters)
}
type Lister[T any] interface {
List(ctx context.Context, filters []Filter) ([]T, error)
}
func List[T Object](ctx context.Context, lister Lister[T], filters []Filter) ([]T, error) {
return lister.List(ctx, filters)
}
type tx struct{}
type instance struct{ id string }
func (i instance) Columns() []*Column {
return []*Column{
{Name: "id", Value: i.id},
}
}
type instanceRepo struct {
instances []*instance
}
type instanceTx struct {
tx
instances []*instance
}
func (ir *instanceRepo) ForTx(t tx) *instanceTx {
return &instanceTx{
tx: t,
instances: slices.Clone(ir.instances),
}
}
var _ Querier[*instance] = (*instanceTx)(nil)
func (it *instanceTx) Get(ctx context.Context, filters []Filter) (*instance, error) {
return nil, nil
}
func (it *instanceTx) List(ctx context.Context, filters []Filter) ([]*instance, error) {
return it.instances, nil
}
type instanceSQLRepo struct{}
func (ir *instanceSQLRepo) ForTx(t tx) *instanceSQLTx {
return &instanceSQLTx{tx: t}
}
type instanceSQLTx struct {
tx
}
func bla() {
var ir instanceRepo
it := ir.ForTx(tx{})
_, _ = Get(context.Background(), it.Get, nil)
}
type Getter2[T Object, C Client3] interface {
Execute(ctx context.Context, client C) error
Result() T
}
type Executor2[C Client3] interface {
Execute(ctx context.Context, client C) error
}
type Client3 interface {
Exec(Executor2[Client3]) error
}

View File

@@ -0,0 +1,3 @@
// package service contains the effective business logic of the application.
// It is the layer that orchestrates the interaction between the domain and the port layer
package service

View File

@@ -0,0 +1,11 @@
package service
type DomainGenerator interface {
GenerateDomain() (string, error)
}
type Domain struct {
Domain string
IsPrimary bool
IsGenerated bool
}

View File

@@ -0,0 +1,5 @@
package service
type IDGenerator interface {
Generate() (id string, err error)
}

View File

@@ -0,0 +1,161 @@
package service
import (
"context"
"github.com/zitadel/zitadel/backend/internal/port"
)
type InstanceService struct {
repo InstanceRepository
domainRepo InstanceDomainRepository
userRepo UserRepository
memberRepo InstanceMemberRepository
inviteRepo InviteInstanceMemberRepository
domainGenerator DomainGenerator
idGenerator IDGenerator
pool port.Pool[Instance]
}
var _ port.Object = Instance{}
type Instance struct {
ID string `consistent:"id,pk"`
Name string `consistent:"name"`
State InstanceState `consistent:"state"`
Domains []*Domain `consistent:"domains"`
}
func (i Instance) Columns() []*port.Column {
return []*port.Column{
{Name: "id", Value: i.ID},
{Name: "name", Value: i.Name},
{Name: "state", Value: i.State},
{Name: "domains", Value: i.Domains},
}
}
type InstanceState uint8
const (
InstanceStateUnspecified InstanceState = iota
InstanceStateActive
InstanceStateRemoved
)
// func NewInstance(
// repo port.InstanceRepository,
// domainGenerator port.DomainGenerator,
// ) *Instance {
// return &Instance{repo: repo}
// }
type SetUpInstanceRequest struct {
Name string
CustomDomain *string
// Admin is the user to be created as the first user of the instance
// If left empty an invite code will be returned to create the admin user
Admin *CreateUserRequest
}
type SetUpInstanceResponse struct {
Instance *Instance
// Admin is the user that was created as the first user of the instance
// If the Admin field in the request was empty this field will be nil
Admin *User
// InviteCode is the invite code that can be used to create the admin user
// If the Admin field in the request was not empty this field will be empty
InviteCode string
}
func (s *InstanceService) SetUpInstance(ctx context.Context, request *SetUpInstanceRequest) (response *SetUpInstanceResponse, err error) {
instance := &Instance{
Name: request.Name,
State: InstanceStateActive,
Domains: make([]*Domain, 0, 2),
}
if request.CustomDomain != nil {
instance.Domains = append(instance.Domains, &Domain{
Domain: *request.CustomDomain,
IsPrimary: false,
IsGenerated: false,
},
)
}
instance.ID, err = s.idGenerator.Generate()
if err != nil {
return nil, err
}
generatedDomain, err := s.domainGenerator.GenerateDomain()
if err != nil {
return nil, err
}
instance.Domains = append(instance.Domains, &Domain{
Domain: generatedDomain,
IsPrimary: true,
IsGenerated: true,
})
var (
user *User
member *InstanceMember
inviteCode string
)
if request.Admin != nil {
user = &User{
Username: request.Admin.Username,
}
user.ID, err = s.idGenerator.Generate()
if err != nil {
return nil, err
}
member = &InstanceMember{
Member: Member{
UserID: user.ID,
},
Roles: []InstanceMemberRole{InstanceMemberRoleOwner, InstanceMemberRoleAdmin},
}
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.End(ctx, err)
if err = s.repo.CreateInstance(ctx, tx, instance); err != nil {
return nil, err
}
if err = s.domainRepo.CreateInstanceDomains(ctx, tx, instance.ID, instance.Domains); err != nil {
return nil, err
}
if user == nil {
inviteCode, err = s.inviteRepo.InviteInstanceMember(ctx, tx, InstanceMemberRoleOwner, InstanceMemberRoleAdmin)
if err != nil {
return nil, err
}
} else {
if err = s.userRepo.CreateUser(ctx, tx, user); err != nil {
return nil, err
}
if err = s.memberRepo.CreateInstanceMember(ctx, tx, member); err != nil {
return nil, err
}
}
return &SetUpInstanceResponse{
Instance: instance,
Admin: user,
InviteCode: inviteCode,
}, nil
}
type InstanceRepository interface {
// CreateInstance creates a new instance
CreateInstance(ctx context.Context, executor port.Executor[Instance], instance *Instance) error
}

View File

@@ -0,0 +1,4 @@
package service
type InstanceDefaultsRepository interface {
}

View File

@@ -0,0 +1,14 @@
package service
import (
"context"
"github.com/zitadel/zitadel/backend/internal/port"
)
type InstanceDomainRepository interface {
// CreateInstanceDomain creates a new instance domain
CreateInstanceDomain(ctx context.Context, executor port.Executor, instanceID string, domain *Domain) error
// CreateInstanceDomains creates multiple instance domains
CreateInstanceDomains(ctx context.Context, executor port.Executor, instanceID string, domains []*Domain) error
}

View File

@@ -0,0 +1,30 @@
package service
import (
"context"
"github.com/zitadel/zitadel/backend/internal/port"
)
type InstanceMember struct {
Member
Roles []InstanceMemberRole
}
type InstanceMemberRole uint8
const (
InstanceMemberRoleUnspecified InstanceMemberRole = iota
InstanceMemberRoleOwner
InstanceMemberRoleAdmin
)
type InstanceMemberRepository interface {
// CreateInstanceMember creates a new instance member
CreateInstanceMember(ctx context.Context, executor port.Executor, member *InstanceMember) error
}
type InviteInstanceMemberRepository interface {
// InviteInstanceMember creates a new invite for an instance admin
InviteInstanceMember(ctx context.Context, executor port.Executor, roles ...InstanceMemberRole) (code string, err error)
}

View File

@@ -0,0 +1,5 @@
package service
type Member struct {
UserID string
}

View File

@@ -0,0 +1,30 @@
package service
import (
"context"
"github.com/zitadel/zitadel/backend/internal/port"
)
type User struct {
ID string `consistent:"id,pk"`
InstanceID string `consistent:"instance_id,pk"`
Username string `consistent:"username"`
}
func (u User) Columns() []*port.Column {
return []*port.Column{
{Name: "id", Value: u.ID},
{Name: "username", Value: u.Username},
}
}
type CreateUserRequest struct {
Username string
}
type UserRepository interface {
// CreateUser creates a new user
CreateUser(ctx context.Context, executor port.Executor[User], user *User) error
}

47
cmd/setup/43.go Normal file
View File

@@ -0,0 +1,47 @@
package setup
import (
"context"
"embed"
_ "embed"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 43/01_table_definition.sql
createOutboxTable string
//go:embed 43/cockroach/*.sql
//go:embed 43/postgres/*.sql
createOutboxTriggers embed.FS
)
type CreateOutbox struct {
dbClient *database.DB
}
func (mig *CreateOutbox) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, createOutboxTable)
if err != nil {
return err
}
statements, err := readStatements(createOutboxTriggers, "43", mig.dbClient.Type())
if err != nil {
return err
}
for _, stmt := range statements {
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
_, err = mig.dbClient.ExecContext(ctx, stmt.query)
if err != nil {
return err
}
}
return nil
}
func (mig *CreateOutbox) String() string {
return "43_create_outbox"
}

View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS event_outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
, instance_id TEXT NOT NULL
, aggregate_type TEXT NOT NULL
, aggregate_id TEXT NOT NULL
, event_type TEXT NOT NULL
, event_revision INT2 NOT NULL
, created_at TIMESTAMPTZ NOT NULL DEFAULT TRANSACTION_TIMESTAMP()
, payload JSONB NULL
, creator TEXT NOT NULL
, position NUMERIC NOT NULL
, in_position_order INT4 NOT NULL
);

View File

@@ -0,0 +1 @@
DROP TRIGGER IF EXISTS copy_to_outbox ON eventstore.events2;

View File

@@ -0,0 +1,29 @@
CREATE OR REPLACE FUNCTION copy_events_to_outbox()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO event_outbox (
instance_id
, aggregate_type
, aggregate_id
, event_type
, event_revision
, created_at
, payload
, creator
, position
, in_position_order
) VALUES (
(NEW).instance_id
, (NEW).aggregate_type
, (NEW).aggregate_id
, (NEW).event_type
, (NEW).revision
, (NEW).created_at
, (NEW).payload
, (NEW).creator
, (NEW).position::NUMERIC
, (NEW).in_tx_order
);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,3 @@
CREATE TRIGGER copy_to_outbox
AFTER INSERT ON eventstore.events2
FOR EACH ROW EXECUTE FUNCTION copy_events_to_outbox();

View File

@@ -0,0 +1,35 @@
DROP TRIGGER IF EXISTS copy_to_outbox ON eventstore.events2;
CREATE OR REPLACE FUNCTION copy_events_to_outbox()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO event_outbox (
instance_id
, aggregate_type
, aggregate_id
, event_type
, event_revision
, created_at
, payload
, creator
, position
, in_position_order
) VALUES (
(NEW).instance_id
, (NEW).aggregate_type
, (NEW).aggregate_id
, (NEW).event_type
, (NEW).revision
, (NEW).created_at
, (NEW).payload
, (NEW).creator
, pg_current_xact_id()::TEXT::NUMERIC
, (NEW).in_tx_order
);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER copy_to_outbox
AFTER INSERT ON eventstore.events2
FOR EACH ROW EXECUTE FUNCTION copy_events_to_outbox();

164
cmd/setup/44.go Normal file
View File

@@ -0,0 +1,164 @@
package setup
import (
"context"
"embed"
_ "embed"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
var (
//go:embed 44/01_table_definition.sql
createTransactionalInstance string
//go:embed 44/cockroach/*.sql
//go:embed 44/postgres/*.sql
createReduceInstanceTrigger embed.FS
)
type CreateTransactionalInstance struct {
dbClient *database.DB
eventstore *eventstore.Eventstore
BulkLimit uint64
}
func (mig *CreateTransactionalInstance) Execute(ctx context.Context, _ eventstore.Event) (err error) {
_, err = mig.dbClient.ExecContext(ctx, createTransactionalInstance)
if err != nil {
return err
}
statements, err := readStatements(createReduceInstanceTrigger, "44", mig.dbClient.Type())
if err != nil {
return err
}
for _, stmt := range statements {
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
_, err = mig.dbClient.ExecContext(ctx, stmt.query)
if err != nil {
return err
}
}
reducer := new(instanceEvents)
for {
err = mig.eventstore.FilterToReducer(ctx,
eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AwaitOpenTransactions().
Limit(mig.BulkLimit).
Offset(reducer.offset).
OrderAsc().
AddQuery().
AggregateTypes(instance.AggregateType).
EventTypes(
instance.InstanceAddedEventType,
instance.InstanceChangedEventType,
instance.InstanceRemovedEventType,
instance.DefaultLanguageSetEventType,
instance.ProjectSetEventType,
instance.ConsoleSetEventType,
instance.DefaultOrgSetEventType,
).
Builder(),
reducer,
)
if err != nil || len(reducer.events) == 0 {
return err
}
tx, err := mig.dbClient.BeginTx(ctx, nil)
if err != nil {
return err
}
for _, event := range reducer.events {
switch e := event.(type) {
case *instance.InstanceAddedEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_added($1, $2, $3, $4)",
e.Aggregate().ID,
e.Name,
e.CreatedAt(),
e.Position(),
)
case *instance.InstanceChangedEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_updated($1, $2, $3, $4)",
e.Aggregate().ID,
e.Name,
e.CreatedAt(),
e.Position(),
)
case *instance.InstanceRemovedEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_removed($1)",
e.Aggregate().ID,
)
case *instance.DefaultLanguageSetEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_removed($1, $2, $3, $4)",
e.Aggregate().ID,
e.Language,
e.CreatedAt(),
e.Position(),
)
case *instance.ProjectSetEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_project_set($1, $2, $3, $4)",
e.Aggregate().ID,
e.ProjectID,
e.CreatedAt(),
e.Position(),
)
case *instance.ConsoleSetEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_console_set($1, $2, $3, $4, $5)",
e.Aggregate().ID,
e.AppID,
e.ClientID,
e.CreatedAt(),
e.Position(),
)
case *instance.DefaultOrgSetEvent:
_, err = tx.ExecContext(ctx,
"SELECT reduce_instance_default_org_set($1, $2, $3, $4)",
e.Aggregate().ID,
e.OrgID,
e.CreatedAt(),
e.Position(),
)
}
if err != nil {
_ = tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
return err
}
}
reducer.events = nil
reducer.offset += uint32(len(reducer.events))
}
}
func (mig *CreateTransactionalInstance) String() string {
return "44_create_transactional_instance"
}
type instanceEvents struct {
offset uint32
events []eventstore.Event
}
// AppendEvents implements eventstore.reducer.
func (i *instanceEvents) AppendEvents(events ...eventstore.Event) {
i.events = append(i.events, events...)
}
// Reduce implements eventstore.reducer.
func (i *instanceEvents) Reduce() error {
return nil
}

View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS instances (
id TEXT PRIMARY KEY
, name TEXT NOT NULL
, change_date TIMESTAMPTZ NOT NULL
, creation_date TIMESTAMPTZ NOT NULL
, latest_position NUMERIC NOT NULL
, default_org_id TEXT
, iam_project_id TEXT
, console_client_id TEXT
, console_app_id TEXT
, default_language TEXT
);
-- | sequence INT8 NOT NULL,

View File

@@ -0,0 +1 @@
DROP TRIGGER IF EXISTS reduce_instance_added ON eventstore.events2;

View File

@@ -0,0 +1,26 @@
CREATE OR REPLACE FUNCTION reduce_instance_added(
id TEXT
, "name" TEXT
, creation_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
INSERT INTO instances (
id
, "name"
, creation_date
, change_date
, latest_position
) VALUES (
id
, "name"
, creation_date
, creation_date
, "position"
)
ON CONFLICT (id) DO NOTHING;
END;
$$;

View File

@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION reduce_instance_default_language_set(
instance_id TEXT
, "language" TEXT
, change_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
UPDATE instances SET
default_language = "language"
, change_date = change_date
, latest_position = "position"
WHERE
id = instance_id
AND latest_position <= "position";
END;
$$;

View File

@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION reduce_instance_project_set(
instance_id TEXT
, project_id TEXT
, change_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
UPDATE instances SET
iam_project_id = project_id
, change_date = change_date
, latest_position = "position"
WHERE
id = instance_id
AND latest_position <= "position";
END;
$$;

View File

@@ -0,0 +1,21 @@
CREATE OR REPLACE FUNCTION reduce_instance_console_set(
instance_id TEXT
, app_id TEXT
, client_id TEXT
, change_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
UPDATE instances SET
console_app_id = app_id
, console_client_id = client_id
, change_date = change_date
, latest_position = "position"
WHERE
id = instance_id
AND latest_position <= "position";
END;
$$;

View File

@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION reduce_instance_default_org_set(
instance_id TEXT
, org_id TEXT
, change_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
UPDATE instances SET
default_org_id = org_id
, change_date = change_date
, latest_position = "position"
WHERE
id = instance_id
AND latest_position <= "position";
END;
$$;

View File

@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION reduce_instance_changed(
instance_id TEXT
, "name" TEXT
, change_date TIMESTAMPTZ
, "position" NUMERIC
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
UPDATE instances SET
"name" = $2
, change_date = change_date
, latest_position = "position"
WHERE
id = instance_id
AND latest_position <= "position";
END;
$$;

View File

@@ -0,0 +1,13 @@
CREATE OR REPLACE FUNCTION reduce_instance_removed(
instance_id TEXT
)
RETURNS VOID
LANGUAGE PLpgSQL
AS $$
BEGIN
DELETE FROM
instances
WHERE
id = instance_id;
END;
$$;

View File

@@ -0,0 +1,56 @@
CREATE OR REPLACE FUNCTION reduce_instance_events()
RETURNS TRIGGER
LANGUAGE PLpgSQL
AS $$
BEGIN
IF (NEW).event_type = 'instance.added' THEN
SELECT reduce_instance_added(
(NEW).aggregate_id
, (NEW).payload->>'name'::TEXT
, (NEW).created_at
, (NEW).position
);
ELSIF (NEW).event_type = 'instance.changed' THEN
SELECT reduce_instance_changed(
(NEW).aggregate_id
, (NEW).payload->>'name'::TEXT
, (NEW).created_at
, (NEW).position
);
ELSIF (NEW).event_type = 'instance.removed' THEN
SELECT reduce_instance_removed(
(NEW).aggregate_id
);
ELSIF (NEW).event_type = 'instance.default.language.set' THEN
SELECT reduce_instance_default_language_set(
(NEW).aggregate_id
, (NEW).payload->>'language'::TEXT
, (NEW).created_at
, (NEW).position
);
ELSIF (NEW).event_type = 'instance.default.org.set' THEN
SELECT reduce_instance_default_org_set(
(NEW).aggregate_id
, (NEW).payload->>'orgId'::TEXT
, (NEW).created_at
, (NEW).position
);
ELSIF (NEW).event_type = 'instance.iam.project.set' THEN
SELECT reduce_instance_project_set(
(NEW).aggregate_id
, (NEW).payload->>'iamProjectId'::TEXT
, (NEW).created_at
, (NEW).position
);
ELSIF (NEW).event_type = 'instance.iam.console.set' THEN
SELECT reduce_instance_console_set(
(NEW).aggregate_id
, (NEW).payload->>'appId'::TEXT
, (NEW).payload->>'clientId'::TEXT
, (NEW).created_at
, (NEW).position
);
END IF;
RETURN NULL;
END
$$;

View File

@@ -0,0 +1,5 @@
CREATE TRIGGER reduce_instance_events
AFTER INSERT ON eventstore.events2
FOR EACH ROW
WHEN (NEW).aggregate_type = 'instance'
EXECUTE FUNCTION reduce_instance_events();

View File

@@ -0,0 +1,26 @@
DROP TRIGGER IF EXISTS reduce_instance_added ON eventstore.events2;
CREATE OR REPLACE FUNCTION zitadel.reduce_instance_added()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO zitadel.instances (
id
, name
, change_date
, creation_date
) VALUES (
(NEW).aggregate_id
, (NEW).payload->>'name'
, (NEW).created_at
, (NEW).created_at
)
ON CONFLICT (id) DO NOTHING;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER reduce_instance_added
AFTER INSERT ON eventstore.events2
FOR EACH ROW
WHEN (NEW).event_type = 'instance.added'
EXECUTE FUNCTION reduce_instance_added();

View File

@@ -128,6 +128,8 @@ type Steps struct {
s38BackChannelLogoutNotificationStart *BackChannelLogoutNotificationStart s38BackChannelLogoutNotificationStart *BackChannelLogoutNotificationStart
s40InitPushFunc *InitPushFunc s40InitPushFunc *InitPushFunc
s42Apps7OIDCConfigsLoginVersion *Apps7OIDCConfigsLoginVersion s42Apps7OIDCConfigsLoginVersion *Apps7OIDCConfigsLoginVersion
s43CreateOutbox *CreateOutbox
s44CreateTransactionalInstance *CreateTransactionalInstance
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {

View File

@@ -171,6 +171,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: esPusherDBClient, esClient: eventstoreClient} steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: esPusherDBClient, esClient: eventstoreClient}
steps.s40InitPushFunc = &InitPushFunc{dbClient: esPusherDBClient} steps.s40InitPushFunc = &InitPushFunc{dbClient: esPusherDBClient}
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: esPusherDBClient} steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: esPusherDBClient}
steps.s43CreateOutbox = &CreateOutbox{dbClient: queryDBClient}
steps.s44CreateTransactionalInstance = &CreateTransactionalInstance{dbClient: queryDBClient, eventstore: eventstoreClient, BulkLimit: config.InitProjections.BulkLimit}
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil) err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections") logging.OnError(err).Fatal("unable to start projections")
@@ -198,6 +200,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
for _, step := range []migration.Migration{ for _, step := range []migration.Migration{
steps.s14NewEventsTable, steps.s14NewEventsTable,
steps.s40InitPushFunc, steps.s40InitPushFunc,
steps.s43CreateOutbox,
steps.s44CreateTransactionalInstance,
steps.s1ProjectionTable, steps.s1ProjectionTable,
steps.s2AssetsTable, steps.s2AssetsTable,
steps.s28AddFieldTable, steps.s28AddFieldTable,