From 3f22fb3a5cec860cf64cd0330dd30a5f18c5bb10 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela <63286031+ahmednfwela@users.noreply.github.com> Date: Thu, 9 Nov 2023 02:38:34 -0800 Subject: [PATCH] feat(user/v1): support composite queries (#6361) * feat(user/v1): support composite queries * fix: added proper error handling for NotQuery * Added error when there are too many levels of nesting * Add localization keys for english * Update internal/api/grpc/user/query.go --- .../api/grpc/management/user_converter.go | 2 +- internal/api/grpc/user/query.go | 37 +- internal/query/search_query.go | 73 +++- internal/query/search_query_test.go | 409 ++++++++++++++++++ internal/query/user.go | 9 + internal/query/user_grant.go | 2 +- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/zitadel/user.proto | 26 ++ 18 files changed, 557 insertions(+), 12 deletions(-) diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index fdcf3ebdbc..d20ad57c4b 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -24,7 +24,7 @@ import ( func ListUsersRequestToModel(req *mgmt_pb.ListUsersRequest) (*query.UserSearchQueries, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - queries, err := user_grpc.UserQueriesToQuery(req.Queries) + queries, err := user_grpc.UserQueriesToQuery(req.Queries, 0 /*start from level 0*/) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index af06c4660d..2e997b2a18 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -7,10 +7,10 @@ import ( user_pb "github.com/zitadel/zitadel/pkg/grpc/user" ) -func UserQueriesToQuery(queries []*user_pb.SearchQuery) (_ []query.SearchQuery, err error) { +func UserQueriesToQuery(queries []*user_pb.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { q := make([]query.SearchQuery, len(queries)) for i, query := range queries { - q[i], err = UserQueryToQuery(query) + q[i], err = UserQueryToQuery(query, level) if err != nil { return nil, err } @@ -18,7 +18,11 @@ func UserQueriesToQuery(queries []*user_pb.SearchQuery) (_ []query.SearchQuery, return q, nil } -func UserQueryToQuery(query *user_pb.SearchQuery) (query.SearchQuery, error) { +func UserQueryToQuery(query *user_pb.SearchQuery, level uint8) (query.SearchQuery, error) { + if level > 20 { + // can't go deeper than 20 levels of nesting. + return nil, errors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.User.TooManyNestingLevels") + } switch q := query.Query.(type) { case *user_pb.SearchQuery_UserNameQuery: return UserNameQueryToQuery(q.UserNameQuery) @@ -42,6 +46,12 @@ func UserQueryToQuery(query *user_pb.SearchQuery) (query.SearchQuery, error) { return ResourceOwnerQueryToQuery(q.ResourceOwner) case *user_pb.SearchQuery_InUserIdsQuery: return InUserIdsQueryToQuery(q.InUserIdsQuery) + case *user_pb.SearchQuery_OrQuery: + return OrQueryToQuery(q.OrQuery, level) + case *user_pb.SearchQuery_AndQuery: + return AndQueryToQuery(q.AndQuery, level) + case *user_pb.SearchQuery_NotQuery: + return NotQueryToQuery(q.NotQuery, level) default: return nil, errors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") } @@ -90,3 +100,24 @@ func ResourceOwnerQueryToQuery(q *user_pb.ResourceOwnerQuery) (query.SearchQuery func InUserIdsQueryToQuery(q *user_pb.InUserIDQuery) (query.SearchQuery, error) { return query.NewUserInUserIdsSearchQuery(q.UserIds) } +func OrQueryToQuery(q *user_pb.OrQuery, level uint8) (query.SearchQuery, error) { + mappedQueries, err := UserQueriesToQuery(q.Queries, level+1) + if err != nil { + return nil, err + } + return query.NewUserOrSearchQuery(mappedQueries) +} +func AndQueryToQuery(q *user_pb.AndQuery, level uint8) (query.SearchQuery, error) { + mappedQueries, err := UserQueriesToQuery(q.Queries, level+1) + if err != nil { + return nil, err + } + return query.NewUserAndSearchQuery(mappedQueries) +} +func NotQueryToQuery(q *user_pb.NotQuery, level uint8) (query.SearchQuery, error) { + mappedQuery, err := UserQueryToQuery(q.Query, level+1) + if err != nil { + return nil, err + } + return query.NewUserNotSearchQuery(mappedQuery) +} diff --git a/internal/query/search_query.go b/internal/query/search_query.go index 73713823f7..c92f155d2a 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -2,6 +2,7 @@ package query import ( "errors" + "fmt" "reflect" "time" @@ -92,27 +93,26 @@ func (q *IsNullQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { func (q *IsNullQuery) comp() sq.Sqlizer { return sq.Eq{q.Column.identifier(): nil} } - func (q *IsNullQuery) Col() Column { return q.Column } -type orQuery struct { +type OrQuery struct { queries []SearchQuery } -func newOrQuery(queries ...SearchQuery) (*orQuery, error) { +func NewOrQuery(queries ...SearchQuery) (*OrQuery, error) { if len(queries) == 0 { return nil, ErrMissingColumn } - return &orQuery{queries: queries}, nil + return &OrQuery{queries: queries}, nil } -func (q *orQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { +func (q *OrQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query.Where(q.comp()) } -func (q *orQuery) comp() sq.Sqlizer { +func (q *OrQuery) comp() sq.Sqlizer { or := make(sq.Or, len(q.queries)) for i, query := range q.queries { or[i] = query.comp() @@ -120,7 +120,66 @@ func (q *orQuery) comp() sq.Sqlizer { return or } -func (q *orQuery) Col() Column { +type AndQuery struct { + queries []SearchQuery +} + +func (q *AndQuery) Col() Column { + return Column{} +} +func NewAndQuery(queries ...SearchQuery) (*AndQuery, error) { + if len(queries) == 0 { + return nil, ErrMissingColumn + } + return &AndQuery{queries: queries}, nil +} + +func (q *AndQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + return query.Where(q.comp()) +} + +func (q *AndQuery) comp() sq.Sqlizer { + and := make(sq.And, len(q.queries)) + for i, query := range q.queries { + and[i] = query.comp() + } + return and +} + +type NotQuery struct { + query SearchQuery +} + +func (q *NotQuery) Col() Column { + return q.query.Col() +} +func NewNotQuery(query SearchQuery) (*NotQuery, error) { + if query == nil { + return nil, ErrMissingColumn + } + return &NotQuery{query: query}, nil +} + +func (q *NotQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + return query.Where(q.comp()) +} + +func (notQ NotQuery) ToSql() (sql string, args []interface{}, err error) { + querySql, queryArgs, queryErr := notQ.query.comp().ToSql() + // Handle the error from the query's ToSql() function. + if queryErr != nil { + return "", queryArgs, queryErr + } + // Construct the SQL statement. + sql = fmt.Sprintf("NOT (%s)", querySql) + return sql, queryArgs, nil +} + +func (q *NotQuery) comp() sq.Sqlizer { + return q +} + +func (q *OrQuery) Col() Column { return Column{} } diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index 5316565653..ac56eb6eee 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -1383,6 +1383,415 @@ func TestNumberComparisonFromMethod(t *testing.T) { } } +func TestNewOrQuery(t *testing.T) { + + type args struct { + queries []SearchQuery + } + + singleCorrectQuery, _ := NewTextQuery(testCol, "hello", TextEquals) + + tests := []struct { + name string + args args + want *OrQuery + wantErr func(error) bool + }{ + { + name: "empty values", + args: args{ + queries: []SearchQuery{}, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrMissingColumn) + }, + }, + { + name: "correct", + args: args{ + queries: []SearchQuery{singleCorrectQuery}, + }, + want: &OrQuery{ + queries: []SearchQuery{singleCorrectQuery}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewOrQuery(tt.args.queries...) + if err != nil && tt.wantErr == nil { + t.Errorf("NewOrQuery() no error expected got %v", err) + return + } else if tt.wantErr != nil && !tt.wantErr(err) { + t.Errorf("NewOrQuery() unexpeted error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewOrQuery() = %v, want %v", got, tt.want) + } + }) + } +} +func TestOrQuery_comp(t *testing.T) { + q1, _ := NewTextQuery(testCol, "hello1", TextEquals) + q2, _ := NewTextQuery(testCol, "hello2", TextEquals) + q3, _ := NewTextQuery(testCol2, "world1", TextEquals) + q4, _ := NewTextQuery(testCol2, "world2", TextEquals) + orq, _ := NewOrQuery(q3, q4) + + type fields struct { + queries []SearchQuery + } + type want struct { + query interface{} + isNil bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "single input", + fields: fields{ + queries: []SearchQuery{q1}, + }, + want: want{ + query: sq.Or{sq.Eq{"test_table.test_col": "hello1"}}, + }, + }, + { + name: "multi input", + fields: fields{ + queries: []SearchQuery{q1, q2}, + }, + want: want{ + query: sq.Or{sq.Eq{"test_table.test_col": "hello1"}, sq.Eq{"test_table.test_col": "hello2"}}, + }, + }, + { + name: "nested inputs", + fields: fields{ + queries: []SearchQuery{q1, orq}, + }, + want: want{ + query: sq.Or{sq.Eq{"test_table.test_col": "hello1"}, sq.Or{sq.Eq{"test_table2.test_col2": "world1"}, sq.Eq{"test_table2.test_col2": "world2"}}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &OrQuery{ + queries: tt.fields.queries, + } + query := s.comp() + if query == nil && tt.want.isNil { + return + } else if tt.want.isNil && query != nil { + t.Error("query should not be nil") + } + + if !reflect.DeepEqual(query, tt.want.query) { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query) + } + }) + } +} +func TestNewAndQuery(t *testing.T) { + + type args struct { + queries []SearchQuery + } + + singleCorrectQuery, _ := NewTextQuery(testCol, "hello", TextEquals) + + tests := []struct { + name string + args args + want *AndQuery + wantErr func(error) bool + }{ + { + name: "empty values", + args: args{ + queries: []SearchQuery{}, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrMissingColumn) + }, + }, + { + name: "correct", + args: args{ + queries: []SearchQuery{singleCorrectQuery}, + }, + want: &AndQuery{ + queries: []SearchQuery{singleCorrectQuery}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewAndQuery(tt.args.queries...) + if err != nil && tt.wantErr == nil { + t.Errorf("NewAndQuery() no error expected got %v", err) + return + } else if tt.wantErr != nil && !tt.wantErr(err) { + t.Errorf("NewAndQuery() unexpeted error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewAndQuery() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAndQuery_comp(t *testing.T) { + q1, _ := NewTextQuery(testCol, "hello1", TextEquals) + q2, _ := NewTextQuery(testCol, "hello2", TextEquals) + q3, _ := NewTextQuery(testCol2, "world1", TextEquals) + q4, _ := NewTextQuery(testCol2, "world2", TextEquals) + andq, _ := NewAndQuery(q3, q4) + + type fields struct { + queries []SearchQuery + } + type want struct { + query interface{} + isNil bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "single input", + fields: fields{ + queries: []SearchQuery{q1}, + }, + want: want{ + query: sq.And{sq.Eq{"test_table.test_col": "hello1"}}, + }, + }, + { + name: "multi input", + fields: fields{ + queries: []SearchQuery{q1, q2}, + }, + want: want{ + query: sq.And{sq.Eq{"test_table.test_col": "hello1"}, sq.Eq{"test_table.test_col": "hello2"}}, + }, + }, + { + name: "nested inputs", + fields: fields{ + queries: []SearchQuery{q1, andq}, + }, + want: want{ + query: sq.And{sq.Eq{"test_table.test_col": "hello1"}, sq.And{sq.Eq{"test_table2.test_col2": "world1"}, sq.Eq{"test_table2.test_col2": "world2"}}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &AndQuery{ + queries: tt.fields.queries, + } + query := s.comp() + if query == nil && tt.want.isNil { + return + } else if tt.want.isNil && query != nil { + t.Error("query should not be nil") + } + + if !reflect.DeepEqual(query, tt.want.query) { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query) + } + }) + } +} + +func TestNewNotQuery(t *testing.T) { + + type args struct { + query SearchQuery + } + + singleCorrectQuery, _ := NewTextQuery(testCol, "hello", TextEquals) + + tests := []struct { + name string + args args + want *NotQuery + wantErr func(error) bool + }{ + { + name: "empty query", + args: args{ + query: nil, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrMissingColumn) + }, + }, + { + name: "correct", + args: args{ + query: singleCorrectQuery, + }, + want: &NotQuery{ + query: singleCorrectQuery, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewNotQuery(tt.args.query) + if err != nil && tt.wantErr == nil { + t.Errorf("NewNotQuery() no error expected got %v", err) + return + } else if tt.wantErr != nil && !tt.wantErr(err) { + t.Errorf("NewNotQuery() unexpeted error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewNotQuery() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNotQuery_comp(t *testing.T) { + q1, _ := NewTextQuery(testCol, "hello1", TextEquals) + + type fields struct { + query SearchQuery + } + type want struct { + query interface{} + isNil bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "single input", + fields: fields{ + query: q1, + }, + want: want{ + query: &NotQuery{query: q1}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &NotQuery{ + query: tt.fields.query, + } + query := s.comp() + if query == nil && tt.want.isNil { + return + } else if tt.want.isNil && query != nil { + t.Error("query should not be nil") + } + + if !reflect.DeepEqual(query, tt.want.query) { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query) + } + }) + } +} +func TestNotQuery_ToSql(t *testing.T) { + q, _ := NewTextQuery(testCol, "hello1", TextEquals) + type fields struct { + query SearchQuery + } + type want struct { + query string + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "single input", + fields: fields{ + query: q, + }, + want: want{ + query: "NOT (test_table.test_col = hello1)", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &NotQuery{ + query: tt.fields.query, + } + queryStr, _, err := s.ToSql() + if err != nil { + t.Error("an error raised ") + } + if queryStr == tt.want.query { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, queryStr, queryStr) + } + }) + } +} + +func TestAndOrQueryCombo(t *testing.T) { + q1, _ := NewTextQuery(testCol, "hello1", TextEquals) + q2, _ := NewTextQuery(testCol, "hello2", TextEquals) + q3, _ := NewTextQuery(testCol2, "world1", TextEquals) + q4, _ := NewTextQuery(testCol2, "world2", TextEquals) + andq, _ := NewAndQuery(q3, q4) + orq, _ := NewOrQuery(q1, q2, andq) + + type fields struct { + query SearchQuery + } + type want struct { + query interface{} + isNil bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "OR containing AND query", + fields: fields{ + query: orq, + }, + want: want{ + query: sq.Or{sq.Eq{"test_table.test_col": "hello1"}, sq.Eq{"test_table.test_col": "hello2"}, sq.And{sq.Eq{"test_table2.test_col2": "world1"}, sq.Eq{"test_table2.test_col2": "world2"}}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := tt.fields.query + query := s.comp() + if query == nil && tt.want.isNil { + return + } else if tt.want.isNil && query != nil { + t.Error("query should not be nil") + } + + if !reflect.DeepEqual(query, tt.want.query) { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query) + } + }) + } +} + func TestNewInTextQuery(t *testing.T) { type args struct { column Column diff --git a/internal/query/user.go b/internal/query/user.go index 2bd2c6bccd..3cd1bad5f9 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -629,6 +629,15 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { return nil } +func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) { + return NewOrQuery(values...) +} +func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) { + return NewAndQuery(values...) +} +func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) { + return NewNotQuery(value) +} func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(UserIDCol, values) } diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 026afbeb5e..0dd366f464 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -149,7 +149,7 @@ func NewUserGrantWithGrantedQuery(owner string) (SearchQuery, error) { if err != nil { return nil, err } - return newOrQuery(orgQuery, projectQuery) + return NewOrQuery(orgQuery, projectQuery) } func NewUserGrantContainsRolesSearchQuery(roles ...string) (SearchQuery, error) { diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 835abf24da..e9290b96bb 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -53,6 +53,7 @@ Errors: Notification: NoDomain: Няма намерен домейн за съобщение User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Потребителят не може да бъде намерен AlreadyExists: Вече съществува потребител NotFoundOnOrg: Потребителят не може да бъде намерен в избраната организация diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 051a925e56..4c8eea97ff 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Keine Domäne für Nachricht gefunden User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Benutzer konnte nicht gefunden werden AlreadyExists: Benutzer existiert bereits NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 63aa8a8fb3..62aaba68fc 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: No Domain found for message User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: User could not be found AlreadyExists: User already exists NotFoundOnOrg: User could not be found on chosen organization diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 9a1a1ff821..69b3bbf261 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: No se encontró el dominio para el mensaje User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: El usuario no pudo encontrarse AlreadyExists: El usuario ya existe NotFoundOnOrg: El usuario no pudo encontrarse en la organización elegida diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index fbd6c2c131..6258812560 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Aucun domaine trouvé pour le message User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: L'utilisateur n'a pas été trouvé AlreadyExists: L'utilisateur existe déjà NotFoundOnOrg: L'utilisateur n'a pas été trouvé dans l'organisation choisie diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index b451d370bd..1407e8a377 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Nessun dominio trovato per il messaggio User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: L'utente non è stato trovato AlreadyExists: L'utente già esistente NotFoundOnOrg: L'utente non è stato trovato nell'organizzazione scelta diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index ec037a7a8e..36b39782c4 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: メッセージのドメインが見つかりません User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: ユーザーが見つかりません AlreadyExists: 既に存在するユーザーです NotFoundOnOrg: ユーザーが選択した組織内で見つかりません diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 51dde7467c..0283fdf4b2 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Не е пронајден домен за пораката User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Корисникот не е пронајден AlreadyExists: Корисникот веќе постои NotFoundOnOrg: Корисникот не е пронајден во избраната организација diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index bb7dc34423..f8bb6cd003 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Nie znaleziono domeny dla wiadomości User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Nie znaleziono użytkownika AlreadyExists: Użytkownik już istnieje NotFoundOnOrg: Użytkownik nie został znaleziony w wybranej organizacji diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 472a7b5bd3..867d15d67c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: Nenhum domínio encontrado para a mensagem User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Usuário não pôde ser encontrado AlreadyExists: Usuário já existe NotFoundOnOrg: Usuário não pôde ser encontrado na organização escolhida diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 4a23798237..d8660c5faf 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -51,6 +51,7 @@ Errors: Notification: NoDomain: 未找到对应的域名 User: + TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: 找不到用户 AlreadyExists: 用户已存在 NotFoundOnOrg: 在所选组织中找不到用户 diff --git a/proto/zitadel/user.proto b/proto/zitadel/user.proto index 493db3acaf..e40f10d0f3 100644 --- a/proto/zitadel/user.proto +++ b/proto/zitadel/user.proto @@ -185,9 +185,35 @@ message SearchQuery { TypeQuery type_query = 8; LoginNameQuery login_name_query = 9; InUserIDQuery in_user_ids_query = 10; + OrQuery or_query = 11; + AndQuery and_query = 12; + NotQuery not_query = 13; } } +message OrQuery { + repeated SearchQuery queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub queries to 'OR'" + } + ]; +} +message AndQuery { + repeated SearchQuery queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub queries to 'AND'" + } + ]; +} + +message NotQuery { + SearchQuery query = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub query to negate (NOT)" + } + ]; +} + message InUserIDQuery { repeated string user_ids = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {