mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-23 22:09:54 +00:00
triggers and backend
This commit is contained in:
2
backend/internal/doc.go
Normal file
2
backend/internal/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// package contains domain logic of Zitadel
|
||||||
|
package internal
|
31
backend/internal/port/instance.go
Normal file
31
backend/internal/port/instance.go
Normal 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)
|
||||||
|
// }
|
75
backend/internal/port/storage.go
Normal file
75
backend/internal/port/storage.go
Normal 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
|
||||||
|
}
|
352
backend/internal/port/storage/consistent/map.go
Normal file
352
backend/internal/port/storage/consistent/map.go
Normal 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
|
||||||
|
}
|
35
backend/internal/port/storage/consistent/map_test.go
Normal file
35
backend/internal/port/storage/consistent/map_test.go
Normal 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},
|
||||||
|
}
|
||||||
|
}
|
7
backend/internal/port/storage/error.go
Normal file
7
backend/internal/port/storage/error.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlreadyExists = errors.New("already exists")
|
||||||
|
)
|
89
backend/internal/port/storage2.go
Normal file
89
backend/internal/port/storage2.go
Normal 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
|
||||||
|
}
|
3
backend/internal/service/doc.go
Normal file
3
backend/internal/service/doc.go
Normal 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
|
11
backend/internal/service/domain.go
Normal file
11
backend/internal/service/domain.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type DomainGenerator interface {
|
||||||
|
GenerateDomain() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Domain struct {
|
||||||
|
Domain string
|
||||||
|
IsPrimary bool
|
||||||
|
IsGenerated bool
|
||||||
|
}
|
5
backend/internal/service/id.go
Normal file
5
backend/internal/service/id.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type IDGenerator interface {
|
||||||
|
Generate() (id string, err error)
|
||||||
|
}
|
161
backend/internal/service/instance.go
Normal file
161
backend/internal/service/instance.go
Normal 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
|
||||||
|
}
|
4
backend/internal/service/instance_defaults.go
Normal file
4
backend/internal/service/instance_defaults.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type InstanceDefaultsRepository interface {
|
||||||
|
}
|
14
backend/internal/service/instance_domain.go
Normal file
14
backend/internal/service/instance_domain.go
Normal 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
|
||||||
|
}
|
30
backend/internal/service/instance_member.go
Normal file
30
backend/internal/service/instance_member.go
Normal 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)
|
||||||
|
}
|
5
backend/internal/service/member.go
Normal file
5
backend/internal/service/member.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
UserID string
|
||||||
|
}
|
30
backend/internal/service/user.go
Normal file
30
backend/internal/service/user.go
Normal 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
47
cmd/setup/43.go
Normal 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"
|
||||||
|
}
|
13
cmd/setup/43/01_table_definition.sql
Normal file
13
cmd/setup/43/01_table_definition.sql
Normal 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
|
||||||
|
);
|
1
cmd/setup/43/cockroach/02_drop_trigger.sql
Normal file
1
cmd/setup/43/cockroach/02_drop_trigger.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TRIGGER IF EXISTS copy_to_outbox ON eventstore.events2;
|
29
cmd/setup/43/cockroach/03_create_function.sql
Normal file
29
cmd/setup/43/cockroach/03_create_function.sql
Normal 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;
|
3
cmd/setup/43/cockroach/04_create_trigger.sql
Normal file
3
cmd/setup/43/cockroach/04_create_trigger.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CREATE TRIGGER copy_to_outbox
|
||||||
|
AFTER INSERT ON eventstore.events2
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION copy_events_to_outbox();
|
35
cmd/setup/43/postgres/02_create_trigger.sql
Normal file
35
cmd/setup/43/postgres/02_create_trigger.sql
Normal 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
164
cmd/setup/44.go
Normal 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
|
||||||
|
}
|
14
cmd/setup/44/01_table_definition.sql
Normal file
14
cmd/setup/44/01_table_definition.sql
Normal 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,
|
1
cmd/setup/44/cockroach/02_drop_trigger.sql
Normal file
1
cmd/setup/44/cockroach/02_drop_trigger.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TRIGGER IF EXISTS reduce_instance_added ON eventstore.events2;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
@@ -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;
|
||||||
|
$$;
|
56
cmd/setup/44/cockroach/04_create_trigger_function.sql
Normal file
56
cmd/setup/44/cockroach/04_create_trigger_function.sql
Normal 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
|
||||||
|
$$;
|
5
cmd/setup/44/cockroach/05_create_trigger.sql
Normal file
5
cmd/setup/44/cockroach/05_create_trigger.sql
Normal 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();
|
26
cmd/setup/44/postgres/02_create_trigger.sql
Normal file
26
cmd/setup/44/postgres/02_create_trigger.sql
Normal 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();
|
@@ -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 {
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user