feat(api): add possibility to retrieve user schemas (#7614)

This PR extends the user schema service (V3 API) with the possibility to ListUserSchemas and GetUserSchemaByID.
The previously started guide is extended to demonstrate how to retrieve the schema(s) and notes the generated revision property.
This commit is contained in:
Livio Spring
2024-03-22 14:26:13 +01:00
committed by GitHub
parent 5b301c7f96
commit 7494a7b6d9
29 changed files with 1597 additions and 19 deletions

View File

@@ -76,6 +76,7 @@ var (
InstanceFeatureProjection *handler.Handler
TargetProjection *handler.Handler
ExecutionProjection *handler.Handler
UserSchemaProjection *handler.Handler
)
type projection interface {
@@ -156,6 +157,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
InstanceFeatureProjection = newInstanceFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_features"]))
TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"]))
ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"]))
UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"]))
newProjectionsList()
return nil
}
@@ -269,5 +271,6 @@ func newProjectionsList() {
InstanceFeatureProjection,
ExecutionProjection,
TargetProjection,
UserSchemaProjection,
}
}

View File

@@ -0,0 +1,203 @@
package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user/schema"
)
const (
UserSchemaTable = "projections.user_schemas"
UserSchemaIDCol = "id"
UserSchemaChangeDateCol = "change_date"
UserSchemaSequenceCol = "sequence"
UserSchemaInstanceIDCol = "instance_id"
UserSchemaStateCol = "state"
UserSchemaTypeCol = "type"
UserSchemaRevisionCol = "revision"
UserSchemaSchemaCol = "schema"
UserSchemaPossibleAuthenticatorsCol = "possible_authenticators"
)
type userSchemaProjection struct{}
func newUserSchemaProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(userSchemaProjection))
}
func (*userSchemaProjection) Name() string {
return UserSchemaTable
}
func (*userSchemaProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
handler.NewTable([]*handler.InitColumn{
handler.NewColumn(UserSchemaIDCol, handler.ColumnTypeText),
handler.NewColumn(UserSchemaChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(UserSchemaSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(UserSchemaStateCol, handler.ColumnTypeEnum),
handler.NewColumn(UserSchemaInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(UserSchemaTypeCol, handler.ColumnTypeText),
handler.NewColumn(UserSchemaRevisionCol, handler.ColumnTypeInt64),
handler.NewColumn(UserSchemaSchemaCol, handler.ColumnTypeJSONB, handler.Nullable()),
handler.NewColumn(UserSchemaPossibleAuthenticatorsCol, handler.ColumnTypeEnumArray, handler.Nullable()),
},
handler.NewPrimaryKey(UserSchemaInstanceIDCol, UserSchemaIDCol),
),
)
}
func (p *userSchemaProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: schema.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: schema.CreatedType,
Reduce: p.reduceCreated,
},
{
Event: schema.UpdatedType,
Reduce: p.reduceUpdated,
},
{
Event: schema.DeactivatedType,
Reduce: p.reduceDeactivated,
},
{
Event: schema.ReactivatedType,
Reduce: p.reduceReactivated,
},
{
Event: schema.DeletedType,
Reduce: p.reduceDeleted,
},
},
},
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(UserSchemaInstanceIDCol),
},
},
},
}
}
func (p *userSchemaProjection) reduceCreated(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*schema.CreatedEvent](event)
if err != nil {
return nil, err
}
return handler.NewCreateStatement(
event,
[]handler.Column{
handler.NewCol(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()),
handler.NewCol(UserSchemaSequenceCol, event.Sequence()),
handler.NewCol(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateActive),
handler.NewCol(UserSchemaTypeCol, e.SchemaType),
handler.NewCol(UserSchemaRevisionCol, 1),
handler.NewCol(UserSchemaSchemaCol, e.Schema),
handler.NewCol(UserSchemaPossibleAuthenticatorsCol, e.PossibleAuthenticators),
},
), nil
}
func (p *userSchemaProjection) reduceUpdated(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*schema.UpdatedEvent](event)
if err != nil {
return nil, err
}
cols := []handler.Column{
handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()),
handler.NewCol(UserSchemaSequenceCol, event.Sequence()),
}
if e.SchemaType != nil {
cols = append(cols, handler.NewCol(UserSchemaTypeCol, *e.SchemaType))
}
if len(e.Schema) > 0 {
cols = append(cols, handler.NewCol(UserSchemaSchemaCol, e.Schema))
cols = append(cols, handler.NewIncrementCol(UserSchemaRevisionCol, 1))
}
if len(e.PossibleAuthenticators) > 0 {
cols = append(cols, handler.NewCol(UserSchemaPossibleAuthenticatorsCol, e.PossibleAuthenticators))
}
return handler.NewUpdateStatement(
event,
cols,
[]handler.Condition{
handler.NewCond(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
},
), nil
}
func (p *userSchemaProjection) reduceDeactivated(event eventstore.Event) (*handler.Statement, error) {
_, err := assertEvent[*schema.DeactivatedEvent](event)
if err != nil {
return nil, err
}
return handler.NewUpdateStatement(
event,
[]handler.Column{
handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()),
handler.NewCol(UserSchemaSequenceCol, event.Sequence()),
handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateInactive),
},
[]handler.Condition{
handler.NewCond(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
},
), nil
}
func (p *userSchemaProjection) reduceReactivated(event eventstore.Event) (*handler.Statement, error) {
_, err := assertEvent[*schema.ReactivatedEvent](event)
if err != nil {
return nil, err
}
return handler.NewUpdateStatement(
event,
[]handler.Column{
handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()),
handler.NewCol(UserSchemaSequenceCol, event.Sequence()),
handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateActive),
},
[]handler.Condition{
handler.NewCond(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
},
), nil
}
func (p *userSchemaProjection) reduceDeleted(event eventstore.Event) (*handler.Statement, error) {
_, err := assertEvent[*schema.DeletedEvent](event)
if err != nil {
return nil, err
}
return handler.NewDeleteStatement(
event,
[]handler.Condition{
handler.NewCond(UserSchemaIDCol, event.Aggregate().ID),
handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID),
},
), nil
}

View File

@@ -0,0 +1,219 @@
package projection
import (
"encoding/json"
"testing"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user/schema"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestUserSchemaProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "reduceCreated",
args: args{
event: getEvent(
testEvent(
schema.CreatedType,
schema.AggregateType,
[]byte(`{"schemaType": "type", "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`),
), eventstore.GenericEventMapper[schema.CreatedEvent]),
},
reduce: (&userSchemaProjection{}).reduceCreated,
want: wantReduce{
aggregateType: eventstore.AggregateType("user_schema"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.user_schemas (id, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
uint64(15),
"instance-id",
domain.UserSchemaStateActive,
"type",
1,
json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword},
},
},
},
},
},
},
{
name: "reduceUpdated",
args: args{
event: getEvent(
testEvent(
schema.CreatedType,
schema.AggregateType,
[]byte(`{"schemaType": "type", "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`),
), eventstore.GenericEventMapper[schema.UpdatedEvent]),
},
reduce: (&userSchemaProjection{}).reduceUpdated,
want: wantReduce{
aggregateType: eventstore.AggregateType("user_schema"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, revision + $5, $6) WHERE (id = $7) AND (instance_id = $8)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"type",
json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`),
1,
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword},
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceDeactivated",
args: args{
event: getEvent(
testEvent(
schema.DeactivatedType,
schema.AggregateType,
nil,
), eventstore.GenericEventMapper[schema.DeactivatedEvent]),
},
reduce: (&userSchemaProjection{}).reduceDeactivated,
want: wantReduce{
aggregateType: eventstore.AggregateType("user_schema"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.UserSchemaStateInactive,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceReactivated",
args: args{
event: getEvent(
testEvent(
schema.ReactivatedType,
schema.AggregateType,
nil,
), eventstore.GenericEventMapper[schema.ReactivatedEvent]),
},
reduce: (&userSchemaProjection{}).reduceReactivated,
want: wantReduce{
aggregateType: eventstore.AggregateType("user_schema"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.UserSchemaStateActive,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceDeleted",
args: args{
event: getEvent(
testEvent(
schema.DeletedType,
schema.AggregateType,
nil,
), eventstore.GenericEventMapper[schema.DeletedEvent]),
},
reduce: (&userSchemaProjection{}).reduceDeleted,
want: wantReduce{
aggregateType: eventstore.AggregateType("user_schema"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_schemas WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceInstanceRemoved",
args: args{
event: getEvent(
testEvent(
instance.InstanceRemovedEventType,
instance.AggregateType,
nil,
), instance.InstanceRemovedEventMapper),
},
reduce: reduceInstanceRemovedHelper(UserSchemaInstanceIDCol),
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_schemas WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, UserSchemaTable, tt.want)
})
}
}