From 1a5180d7d36049cf738f8711d93cd7b508f8f093 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Wed, 20 Jul 2022 22:33:24 +0300 Subject: [PATCH] added support to filter request.user.profile relation fields --- apis/record.go | 30 +++- resolvers/record_field_resolver.go | 99 +++++++++---- resolvers/record_field_resolver_test.go | 179 +++++++++++++++--------- tests/data/data.db | Bin 159744 -> 159744 bytes tools/search/provider.go | 14 +- tools/search/provider_test.go | 44 +++++- 6 files changed, 260 insertions(+), 106 deletions(-) diff --git a/apis/record.go b/apis/record.go index a1f40cde..8493ac8e 100644 --- a/apis/record.go +++ b/apis/record.go @@ -52,10 +52,9 @@ func (api *recordApi) list(c echo.Context) error { return rest.NewForbiddenError("Only admins can perform this action.", nil) } - // forbid user/guest defined non-relational joins (aka. @collection.*) - queryStr := c.QueryString() - if admin == nil && queryStr != "" && (strings.Contains(queryStr, "@collection") || strings.Contains(queryStr, "%40collection")) { - return rest.NewForbiddenError("Only admins can filter by @collection.", nil) + // forbid users and guests to query special filter/sort fields + if err := api.checkForForbiddenQueryFields(c); err != nil { + return err } requestData := api.exportRequestData(c) @@ -63,14 +62,15 @@ func (api *recordApi) list(c echo.Context) error { fieldsResolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) searchProvider := search.NewProvider(fieldsResolver). - Query(api.app.Dao().RecordQuery(collection)) + Query(api.app.Dao().RecordQuery(collection)). + CountColumn(fmt.Sprintf("%s.id", api.app.Dao().DB().QuoteSimpleColumnName(collection.Name))) if admin == nil && collection.ListRule != nil { searchProvider.AddFilter(search.FilterData(*collection.ListRule)) } var rawRecords = []dbx.NullStringMap{} - result, err := searchProvider.ParseAndExec(queryStr, &rawRecords) + result, err := searchProvider.ParseAndExec(c.QueryString(), &rawRecords) if err != nil { return rest.NewBadRequestError("Invalid filter parameters.", err) } @@ -407,6 +407,24 @@ func (api *recordApi) exportRequestData(c echo.Context) map[string]any { return result } +func (api *recordApi) checkForForbiddenQueryFields(c echo.Context) error { + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin != nil { + return nil // admins are allowed to query everything + } + + decodedQuery := c.QueryParam(search.FilterQueryParam) + c.QueryParam(search.SortQueryParam) + forbiddenFields := []string{"@collection.", "@request."} + + for _, field := range forbiddenFields { + if strings.Contains(decodedQuery, field) { + return rest.NewForbiddenError("Only admins can filter by @collection and @request query params", nil) + } + } + + return nil +} + func (api *recordApi) expandFunc(c echo.Context, requestData map[string]any) daos.ExpandFetchFunc { admin, _ := c.Get(ContextAdminKey).(*models.Admin) diff --git a/resolvers/record_field_resolver.go b/resolvers/record_field_resolver.go index 3e3a42b3..c001a7e9 100644 --- a/resolvers/record_field_resolver.go +++ b/resolvers/record_field_resolver.go @@ -20,6 +20,7 @@ import ( var _ search.FieldResolver = (*RecordFieldResolver)(nil) type join struct { + id string table string on dbx.Expression } @@ -36,7 +37,7 @@ type RecordFieldResolver struct { baseCollection *models.Collection allowedFields []string requestData map[string]any - joins map[string]join + joins []join // we cannot use a map because the insertion order is not preserved loadedCollections []*models.Collection } @@ -50,7 +51,7 @@ func NewRecordFieldResolver( dao: dao, baseCollection: baseCollection, requestData: requestData, - joins: make(map[string]join), + joins: []join{}, loadedCollections: []*models.Collection{baseCollection}, allowedFields: []string{ `^\w+[\w\.]*$`, @@ -85,6 +86,7 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { // id // project.screen.status // @request.status +// @request.user.profile.someRelation.name // @collection.product.name func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) { if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) { @@ -93,15 +95,6 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac props := strings.Split(fieldName, ".") - // check for @request field - if props[0] == "@request" { - if len(props) == 1 { - return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName) - } - - return r.resolveRequestField(props[1:]...) - } - currentCollectionName := r.baseCollection.Name currentTableAlias := currentCollectionName @@ -113,16 +106,54 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac } currentCollectionName = props[1] - currentTableAlias = "c_" + currentCollectionName + currentTableAlias = "__collection_" + currentCollectionName collection, err := r.loadCollection(currentCollectionName) if err != nil { return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName) } - r.addJoin(collection.Name, currentTableAlias, "", "", "") + r.addJoin(collection.Name, currentTableAlias, nil) props = props[2:] // leave only the collection fields + } else if props[0] == "@request" { + // check for @request field + if len(props) == 1 { + return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName) + } + + // not a profile relational field + if len(props) <= 4 || !strings.HasPrefix(fieldName, "@request.user.profile.") { + return r.resolveStaticRequestField(props[1:]...) + } + + // resolve the profile collection fields + currentCollectionName = models.ProfileCollectionName + currentTableAlias = "__user_" + currentCollectionName + + collection, err := r.loadCollection(currentCollectionName) + if err != nil { + return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName) + } + + profileIdPlaceholder, profileIdPlaceholderParam, err := r.resolveStaticRequestField("user", "profile", "id") + if err != nil { + return "", nil, fmt.Errorf("Failed to resolve @request.user.profile.id path in %q.", fieldName) + } + if strings.ToLower(profileIdPlaceholder) == "null" { + // the user doesn't have an associated profile + return "NULL", nil, nil + } + + // join the profile collection + r.addJoin(collection.Name, currentTableAlias, dbx.NewExp(fmt.Sprintf( + // aka. profiles.id = profileId + "[[%s.id]] = %s", + inflector.Columnify(currentTableAlias), + profileIdPlaceholder, + ), profileIdPlaceholderParam)) + + props = props[3:] // leave only the profile fields } baseModelFields := schema.ReservedFieldNames() @@ -173,9 +204,14 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac r.addJoin( newCollectionName, newTableAlias, - "id", - currentTableAlias, - field.Name, + dbx.NewExp(fmt.Sprintf( + // 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array) + "[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')", + inflector.Columnify(currentTableAlias), + inflector.Columnify(field.Name), + inflector.Columnify(newTableAlias), + inflector.Columnify("id"), + )), ) currentCollectionName = newCollectionName @@ -185,7 +221,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac return "", nil, fmt.Errorf("Failed to resolve field %q.", fieldName) } -func (r *RecordFieldResolver) resolveRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) { +func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) { // ignore error because requestData is dynamic and some of the // lookup keys may not be defined for the request resultVal, _ := extractNestedMapVal(r.requestData, path...) @@ -259,24 +295,27 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models return collection, nil } -func (r *RecordFieldResolver) addJoin(tableName, tableAlias, fieldName, ref, refFieldName string) { - table := fmt.Sprintf( +func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) { + tableExpr := fmt.Sprintf( "%s %s", inflector.Columnify(tableName), inflector.Columnify(tableAlias), ) - var on dbx.Expression - if ref != "" { - on = dbx.NewExp(fmt.Sprintf( - // 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array) - "[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')", - inflector.Columnify(ref), - inflector.Columnify(refFieldName), - inflector.Columnify(tableAlias), - inflector.Columnify(fieldName), - )) + join := join{ + id: tableAlias, + table: tableExpr, + on: on, } - r.joins[tableAlias] = join{table, on} + // replace existing join + for i, j := range r.joins { + if j.id == join.id { + r.joins[i] = join + return + } + } + + // register new join + r.joins = append(r.joins, join) } diff --git a/resolvers/record_field_resolver_test.go b/resolvers/record_field_resolver_test.go index f6e86071..d6c7066d 100644 --- a/resolvers/record_field_resolver_test.go +++ b/resolvers/record_field_resolver_test.go @@ -2,11 +2,12 @@ package resolvers_test import ( "encoding/json" - "strings" + "regexp" "testing" "github.com/pocketbase/pocketbase/resolvers" "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" ) func TestRecordFieldResolverUpdateQuery(t *testing.T) { @@ -18,54 +19,94 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { t.Fatal(err) } + requestData := map[string]any{ + "user": map[string]any{ + "id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + "profile": map[string]any{ + "id": "d13f60a4-5765-48c7-9e1d-3e782340f833", + "name": "test", + }, + }, + } + scenarios := []struct { - fieldName string - expectQueryParts []string // we are matching parts of the query - // since joins are added with map iteration and the order is not guaranteed + name string + fields []string + expectQuery string }{ - // missing field - {"", []string{ + { + "missing field", + []string{""}, "SELECT `demo4`.* FROM `demo4`", - }}, - // non relation field - {"title", []string{ + }, + { + "non relation field", + []string{"title"}, "SELECT `demo4`.* FROM `demo4`", - }}, - // incomplete rel - {"onerel", []string{ + }, + { + "incomplete rel", + []string{"onerel"}, "SELECT `demo4`.* FROM `demo4`", - }}, - // single rel - {"onerel.title", []string{ - "SELECT DISTINCT `demo4`.* FROM `demo4`", - " LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')", - }}, - // nested incomplete rels - {"manyrels.onerel", []string{ - "SELECT DISTINCT `demo4`.* FROM `demo4`", - " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", - }}, - // nested complete rels - {"manyrels.onerel.title", []string{ - "SELECT DISTINCT `demo4`.* FROM `demo4`", - " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", - " LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')", - }}, - // // repeated nested rels - {"manyrels.onerel.manyrels.onerel.title", []string{ - "SELECT DISTINCT `demo4`.* FROM `demo4`", - " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", - " LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')", - " LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%')", - " LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')", - }}, + }, + { + "single rel", + []string{"onerel.title"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')", + }, + { + "non-relation field + single rel", + []string{"title", "onerel.title"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')", + }, + { + "nested incomplete rels", + []string{"manyrels.onerel"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", + }, + { + "nested complete rels", + []string{"manyrels.onerel.title"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')", + }, + { + "repeated nested rels", + []string{"manyrels.onerel.manyrels.onerel.title"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')", + }, + { + "multiple rels", + []string{"manyrels.title", "onerel.onefile"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')", + }, + { + "@collection join", + []string{"@collection.demo.title", "@collection.demo2.text", "@collection.demo.file"}, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo` `__collection_demo` LEFT JOIN `demo2` `__collection_demo2`", + }, + { + "static @request.user.profile fields", + []string{"@request.user.id", "@request.user.profile.id", "@request.data.demo"}, + "SELECT `demo4`.* FROM `demo4`", + }, + { + "relational @request.user.profile fields", + []string{"@request.user.profile.rel.id", "@request.user.profile.rel.name"}, + "^" + + regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") + + " {:.*} " + + regexp.QuoteMeta("LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles.rel]] LIKE ('%' || [[__user_profiles_rel.id]] || '%')") + + "$", + }, } for i, s := range scenarios { query := app.Dao().RecordQuery(collection) - r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil) - r.Resolve(s.fieldName) + r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData) + for _, field := range s.fields { + r.Resolve(field) + } if err := r.UpdateQuery(query); err != nil { t.Errorf("(%d) UpdateQuery failed with error %v", i, err) @@ -74,16 +115,8 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { rawQuery := query.Build().SQL() - partsLength := 0 - for _, part := range s.expectQueryParts { - partsLength += len(part) - if !strings.Contains(rawQuery, part) { - t.Errorf("(%d) Part %v is missing from query \n%v", i, part, rawQuery) - } - } - - if partsLength != len(rawQuery) { - t.Errorf("(%d) Expected %d characters, got %d in \n%v", i, partsLength, len(rawQuery), rawQuery) + if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) { + t.Errorf("(%d) Expected query\n %v \ngot:\n %v", i, s.expectQuery, rawQuery) } } } @@ -97,7 +130,16 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) { t.Fatal(err) } - r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil) + requestData := map[string]any{ + "user": map[string]any{ + "id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + "profile": map[string]any{ + "id": "d13f60a4-5765-48c7-9e1d-3e782340f833", + }, + }, + } + + r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData) scenarios := []struct { fieldName string @@ -118,42 +160,45 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) { {"manyrels.unknown", true, ""}, {"manyrels.title", false, "[[demo4_manyrels.title]]"}, {"manyrels.onerel.manyrels.onefile", false, "[[demo4_manyrels_onerel_manyrels.onefile]]"}, + // @request.user.profile relation join: + {"@request.user.profile.rel.name", false, "[[__user_profiles_rel.name]]"}, + // @collection fieds: {"@collect", true, ""}, {"collection.demo4.title", true, ""}, {"@collection", true, ""}, {"@collection.unknown", true, ""}, {"@collection.demo", true, ""}, {"@collection.demo.", true, ""}, - {"@collection.demo.title", false, "[[c_demo.title]]"}, - {"@collection.demo4.title", false, "[[c_demo4.title]]"}, - {"@collection.demo4.id", false, "[[c_demo4.id]]"}, - {"@collection.demo4.created", false, "[[c_demo4.created]]"}, - {"@collection.demo4.updated", false, "[[c_demo4.updated]]"}, + {"@collection.demo.title", false, "[[__collection_demo.title]]"}, + {"@collection.demo4.title", false, "[[__collection_demo4.title]]"}, + {"@collection.demo4.id", false, "[[__collection_demo4.id]]"}, + {"@collection.demo4.created", false, "[[__collection_demo4.created]]"}, + {"@collection.demo4.updated", false, "[[__collection_demo4.updated]]"}, {"@collection.demo4.manyrels.missing", true, ""}, - {"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[c_demo4_manyrels_onerel_manyrels_onerel.onefile]]"}, + {"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[__collection_demo4_manyrels_onerel_manyrels_onerel.onefile]]"}, } - for i, s := range scenarios { + for _, s := range scenarios { name, params, err := r.Resolve(s.fieldName) hasErr := err != nil if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + t.Errorf("(%q) Expected hasErr %v, got %v (%v)", s.fieldName, s.expectError, hasErr, err) continue } if name != s.expectName { - t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name) + t.Errorf("(%q) Expected name %q, got %q", s.fieldName, s.expectName, name) } // params should be empty for non @request fields if len(params) != 0 { - t.Errorf("(%d) Expected 0 params, got %v", i, params) + t.Errorf("(%q) Expected 0 params, got %v", s.fieldName, params) } } } -func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) { +func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() @@ -171,7 +216,13 @@ func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) { "b": 456, "c": map[string]int{"sub": 1}, }, - "user": nil, + "user": map[string]any{ + "id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + "profile": map[string]any{ + "id": "d13f60a4-5765-48c7-9e1d-3e782340f833", + "name": "test", + }, + }, } r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData) @@ -194,7 +245,9 @@ func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) { {"@request.data.b.missing", false, ``}, {"@request.data.c", false, `"{\"sub\":1}"`}, {"@request.user", true, ""}, - {"@request.user.id", false, ""}, + {"@request.user.id", false, `"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`}, + {"@request.user.profile", false, `"{\"id\":\"d13f60a4-5765-48c7-9e1d-3e782340f833\",\"name\":\"test\"}"`}, + {"@request.user.profile.name", false, `"test"`}, } for i, s := range scenarios { diff --git a/tests/data/data.db b/tests/data/data.db index 752a0e6604e2a74fe6f12da725f99fb1b1450e92..f84fd27e8f657565bf566b7df96b7d081130e572 100644 GIT binary patch delta 394 zcmZp8z}fJCbAq&>Dgy(9IuHv2F*6VsP1G@FRNa^`e?KGhrUSeIjAu3r3cTT+{!5mz zlikNUl-D|xWBNvWM)B$5@{B6NMg~Skx(23=+RU6l8Bw5QEL_%r-^f70(A>(z$jZc0 z&&b4h@|}Jarf8+ER! zWG60knI59Rq`w_VoaE$emSExy3IhX!IuHv2F*6VsOw=)ERN0s?e?KG3rUSeIj9)hk3cTT+{!5mz zlfBeBl-D|xWBNvWM)B$IeB1RLY%?bh%jO;DV&2{4J;@eLxWi;kwX<`s$ z-_E$6k-3nEg&~B2XDzo7LkO4b#Dy-?Lll_wr#q}+WSz{rS$uke8WZdG%L+`DoYOt6 zn7(fJ(PEm-$k`Ua$Q#Nj+nKukgErGVCdSz53-p= 2"}, + "", false, `{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, []string{ - "SELECT count(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC", + "SELECT COUNT(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC", "SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC LIMIT 200", }, }, @@ -283,10 +298,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { 10, []SortField{{"test3", SortAsc}}, []FilterData{"test3 != ''"}, + "", false, `{"page":1,"perPage":10,"totalItems":0,"items":[]}`, []string{ - "SELECT count(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC", + "SELECT COUNT(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC", "SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10", }, }, @@ -296,10 +312,25 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { 1, []SortField{}, []FilterData{}, + "", false, `{"page":2,"perPage":1,"totalItems":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, []string{ - "SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", + "SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", + "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1", + }, + }, + // distinct count column + { + 3, + 1, + []SortField{}, + []FilterData{}, + "test.test1", + false, + `{"page":2,"perPage":1,"totalItems":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + []string{ + "SELECT COUNT(DISTINCT(test.test1)) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1", }, }, @@ -314,7 +345,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { Page(s.page). PerPage(s.perPage). Sort(s.sort). - Filter(s.filter) + Filter(s.filter). + CountColumn(s.countColumn) result, err := p.Exec(&[]testTableStruct{})