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

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
}