From 01c293bf40df8eb36802080671cc6884788a3dfa Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 13:42:41 -0700 Subject: [PATCH] provide mechanism to call functions when doing aggregations. provide a standardized format for token aggregation ($.system|$.code) --- .../pkg/database/sqlite_repository_query.go | 112 ++++++++++----- .../sqlite_repository_query_sql_test.go | 135 ++++++++++++++++-- backend/pkg/models/query_resource.go | 55 ++++--- backend/pkg/models/query_resource_test.go | 17 ++- .../pkg/web/handler/dashboard/default.json | 6 +- 5 files changed, 257 insertions(+), 68 deletions(-) diff --git a/backend/pkg/database/sqlite_repository_query.go b/backend/pkg/database/sqlite_repository_query.go index 36832008..3740c850 100644 --- a/backend/pkg/database/sqlite_repository_query.go +++ b/backend/pkg/database/sqlite_repository_query.go @@ -18,18 +18,19 @@ import ( type SearchParameterType string const ( - SearchParameterTypeNumber SearchParameterType = "number" - SearchParameterTypeDate SearchParameterType = "date" + //simple types + SearchParameterTypeNumber SearchParameterType = "number" + SearchParameterTypeDate SearchParameterType = "date" + SearchParameterTypeUri SearchParameterType = "uri" + SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive. + + //complex types SearchParameterTypeString SearchParameterType = "string" SearchParameterTypeToken SearchParameterType = "token" SearchParameterTypeReference SearchParameterType = "reference" - SearchParameterTypeUri SearchParameterType = "uri" SearchParameterTypeQuantity SearchParameterType = "quantity" SearchParameterTypeComposite SearchParameterType = "composite" SearchParameterTypeSpecial SearchParameterType = "special" - - SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive. - ) const TABLE_ALIAS = "fhir" @@ -58,7 +59,7 @@ func (sr *SqliteRepository) QueryResources(ctx context.Context, query models.Que return nil, err } - if query.Aggregations != nil && (len(query.Aggregations.GroupBy) > 0 || len(query.Aggregations.CountBy) > 0) { + if query.Aggregations != nil && (query.Aggregations.GroupBy != nil || query.Aggregations.CountBy != nil) { results := []map[string]interface{}{} clientResp := sqlQuery.Find(&results) return results, clientResp.Error @@ -148,27 +149,30 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. //Handle Aggregations - if len(query.Aggregations.CountBy) > 0 { + if query.Aggregations.CountBy != nil { //populate the group by and order by clause with the count by values - query.Aggregations.OrderBy = "count(*) DESC" + query.Aggregations.OrderBy = &models.QueryResourceAggregation{ + Field: "*", + Function: "count", + } query.Aggregations.GroupBy = query.Aggregations.CountBy - if query.Aggregations.GroupBy == "*" { + if query.Aggregations.GroupBy.Field == "*" { //we need to get the count of all resources, so we need to remove the group by clause and replace it by // `source_resource_type` which will be the same for all resources - query.Aggregations.GroupBy = "source_resource_type" + query.Aggregations.GroupBy.Field = "source_resource_type" } } //process order by clause - if len(query.Aggregations.OrderBy) > 0 { + if query.Aggregations.OrderBy != nil { orderAsc := true //default to ascending, switch to desc if parameter is a date type. - if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") { - orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup) + if !(query.Aggregations.OrderBy.Field == "*") { + orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup) if err != nil { return nil, err } - orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam) + orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam.SearchParameter) if err != nil { return nil, err } @@ -186,17 +190,17 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. orderClause = fmt.Sprintf("%s DESC", orderClause) } } else { - orderClause = query.Aggregations.OrderBy + orderClause = fmt.Sprintf("%s(%s) DESC", query.Aggregations.OrderBy.Function, query.Aggregations.OrderBy.Field) } } //process group by clause - if len(query.Aggregations.GroupBy) > 0 { - groupAggregationParam, err := ProcessAggregationParameter(query.Aggregations.GroupBy, searchCodeToTypeLookup) + if query.Aggregations.GroupBy != nil { + groupAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.GroupBy, searchCodeToTypeLookup) if err != nil { return nil, err } - groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam) + groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam.SearchParameter) if err != nil { return nil, err } @@ -205,8 +209,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. groupClause = AggregationParameterToClause(groupAggregationParam) selectClauses = []string{ fmt.Sprintf("%s as %s", groupClause, "label"), - "count(*) as value", } + + if query.Aggregations.OrderBy == nil || query.Aggregations.OrderBy.Field == "*" { + selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", "count(*)", "value")) + orderClause = fmt.Sprintf("%s DESC", "count(*)") + } else { + //use the orderBy aggregation as the value + orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup) + if err != nil { + return nil, err + } + + orderSelectClause := AggregationParameterToClause(orderAggregationParam) + selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", orderSelectClause, "value")) + } + } } @@ -216,21 +234,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. fromClauses = lo.Uniq(fromClauses) fromClauses = lo.Compact(fromClauses) - fluentQuery := sr.GormClient.WithContext(ctx). + sqlQuery := sr.GormClient.WithContext(ctx). Select(strings.Join(selectClauses, ", ")). Where(strings.Join(whereClauses, " AND "), whereNamedParameters). Group(groupClause). - Order(orderClause) + Order(orderClause). + Table(strings.Join(fromClauses, ", ")) //add limit and offset clauses if present if query.Limit != nil { - fluentQuery = fluentQuery.Limit(*query.Limit) + sqlQuery = sqlQuery.Limit(*query.Limit) } if query.Offset != nil { - fluentQuery = fluentQuery.Offset(*query.Offset) + sqlQuery = sqlQuery.Offset(*query.Offset) } - return fluentQuery.Table(strings.Join(fromClauses, ", ")), nil + return sqlQuery, nil } /// INTERNAL functionality. These functions are exported for testing, but are not available in the Interface @@ -242,6 +261,11 @@ type SearchParameter struct { Modifier string } +type AggregationParameter struct { + SearchParameter + Function string //count, sum, avg, min, max, etc +} + // Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together // For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree // @@ -584,14 +608,31 @@ func SearchCodeToFromClause(searchParam SearchParameter) (string, error) { return "", nil } -func AggregationParameterToClause(aggParameter SearchParameter) string { +func AggregationParameterToClause(aggParameter AggregationParameter) string { + var clause string + switch aggParameter.Type { - case SearchParameterTypeQuantity, SearchParameterTypeToken, SearchParameterTypeString: + case SearchParameterTypeQuantity, SearchParameterTypeString: //setup the clause - return fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + case SearchParameterTypeToken: + //modifier is optional for token types. + if aggParameter.Modifier != "" { + clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + } else { + //if no modifier is specified, use the system and code to generate the clause + //((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) + clause = fmt.Sprintf("((%sJson.value ->> '$.system') || '|' || (%sJson.value ->> '$.code'))", aggParameter.Name, aggParameter.Name) + } + default: - return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name) + clause = fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name) } + + if len(aggParameter.Function) > 0 { + clause = fmt.Sprintf("%s(%s)", aggParameter.Function, clause) + } + return clause } // ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties: @@ -602,12 +643,15 @@ func AggregationParameterToClause(aggParameter SearchParameter) string { // eg. `identifier:code` // // if the a property is specified, its set as the modifier, and used when generating the SQL query groupBy, orderBy, etc clause -func ProcessAggregationParameter(aggregationFieldWithProperty string, searchParamTypeLookup map[string]string) (SearchParameter, error) { - aggregationParameter := SearchParameter{} +func ProcessAggregationParameter(aggregationFieldWithFn models.QueryResourceAggregation, searchParamTypeLookup map[string]string) (AggregationParameter, error) { + aggregationParameter := AggregationParameter{ + SearchParameter: SearchParameter{}, + Function: aggregationFieldWithFn.Function, + } //determine the searchCode searchCodeModifier //TODO: this is only applicable to string, token, reference and uri type (however unknown names & modifiers are ignored) - if aggregationFieldParts := strings.SplitN(aggregationFieldWithProperty, ":", 2); len(aggregationFieldParts) == 2 { + if aggregationFieldParts := strings.SplitN(aggregationFieldWithFn.Field, ":", 2); len(aggregationFieldParts) == 2 { aggregationParameter.Name = aggregationFieldParts[0] aggregationParameter.Modifier = aggregationFieldParts[1] } else { @@ -618,7 +662,7 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara //next, determine the searchCodeType for this Resource (or throw an error if it is unknown) searchParamTypeStr, searchParamTypeOk := searchParamTypeLookup[aggregationParameter.Name] if !searchParamTypeOk { - return aggregationParameter, fmt.Errorf("unknown search parameter: %s", aggregationParameter.Name) + return aggregationParameter, fmt.Errorf("unknown search parameter in aggregation: %s", aggregationParameter.Name) } else { aggregationParameter.Type = SearchParameterType(searchParamTypeStr) } @@ -628,6 +672,8 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara if len(aggregationParameter.Modifier) > 0 { return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier) } + } else if aggregationParameter.Type == SearchParameterTypeToken { + //modifier is optional for token types } else { //complex types must have a modifier if len(aggregationParameter.Modifier) == 0 { diff --git a/backend/pkg/database/sqlite_repository_query_sql_test.go b/backend/pkg/database/sqlite_repository_query_sql_test.go index 51e61b6a..c872f97d 100644 --- a/backend/pkg/database/sqlite_repository_query_sql_test.go +++ b/backend/pkg/database/sqlite_repository_query_sql_test.go @@ -98,7 +98,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL() { "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", "GROUP BY `fhir`.`id`", - "ORDER BY fhir.sort_date ASC", + "ORDER BY fhir.sort_date DESC", }, " "), sqlString) require.Equal(suite.T(), sqlParams, []interface{}{ @@ -136,7 +136,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithMultipleWhereCon "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson, json_each(fhir.category) as categoryJson", "WHERE ((codeJson.value ->> '$.code' = ?)) AND ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)", "GROUP BY `fhir`.`id`", - "ORDER BY fhir.sort_date ASC", + "ORDER BY fhir.sort_date DESC", }, " "), sqlString) require.Equal(suite.T(), sqlParams, []interface{}{ @@ -158,7 +158,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderBy "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{OrderBy: "instantiatesUri"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -193,7 +193,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordOrderByAg Select: []string{}, Where: map[string]interface{}{}, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{OrderBy: "id"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "id"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -230,7 +230,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexOrderByAg "code": "test_code", }, From: "Observation", - Aggregations: &models.QueryResourceAggregations{OrderBy: "valueString:value"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "valueString:value"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -267,7 +267,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveCountBy "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "instantiatesUri"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -304,7 +304,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordCountByAg "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "source_resource_type"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "source_resource_type"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -339,7 +339,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithWildcardCountByA Select: []string{}, Where: map[string]interface{}{}, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "*"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "*"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -376,7 +376,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg "code": "test_code", }, From: "Observation", - Aggregations: &models.QueryResourceAggregations{CountBy: "code:code"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "code:code"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -398,3 +398,120 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg "test_code", "00000000-0000-0000-0000-000000000000", }) } + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexGroupByWithOrderByMaxFnAggregation() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{ + "code": "test_code", + }, + From: "Observation", + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "code:code"}, + OrderBy: &models.QueryResourceAggregation{Field: "sort_date", Function: "max"}, + }, + }) + require.NoError(suite.T(), err) + var results []map[string]interface{} + statement := sqlQuery.Find(&results).Statement + sqlString := statement.SQL.String() + sqlParams := statement.Vars + + //assert + require.NoError(suite.T(), err) + require.Equal(suite.T(), + strings.Join([]string{ + "SELECT (codeJson.value ->> '$.code') as label, max(fhir.sort_date) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", + "GROUP BY (codeJson.value ->> '$.code')", + "ORDER BY max(fhir.sort_date) DESC", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "test_code", "00000000-0000-0000-0000-000000000000", + }) +} + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifier() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{}, + From: "Observation", + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "code"}, + }, + }) + require.NoError(suite.T(), err) + var results []map[string]interface{} + statement := sqlQuery.Find(&results).Statement + sqlString := statement.SQL.String() + sqlParams := statement.Vars + + //assert + require.NoError(suite.T(), err) + require.Equal(suite.T(), + strings.Join([]string{ + "SELECT ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE (user_id = ?)", + "GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))", + "ORDER BY count(*) DESC", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "00000000-0000-0000-0000-000000000000", + }) +} + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifierWithLimit() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + limit := 10 + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{}, + From: "Observation", + Limit: &limit, + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "code"}, + }, + }) + require.NoError(suite.T(), err) + var results []map[string]interface{} + statement := sqlQuery.Find(&results).Statement + sqlString := statement.SQL.String() + sqlParams := statement.Vars + + //assert + require.NoError(suite.T(), err) + require.Equal(suite.T(), + strings.Join([]string{ + "SELECT ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE (user_id = ?)", + "GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))", + "ORDER BY count(*) DESC", + "LIMIT 10", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "00000000-0000-0000-0000-000000000000", + }) +} diff --git a/backend/pkg/models/query_resource.go b/backend/pkg/models/query_resource.go index 64a4dac8..88c3f584 100644 --- a/backend/pkg/models/query_resource.go +++ b/backend/pkg/models/query_resource.go @@ -19,10 +19,15 @@ type QueryResource struct { } type QueryResourceAggregations struct { - CountBy string `json:"count_by"` //alias for both groupby and orderby, cannot be used together + CountBy *QueryResourceAggregation `json:"count_by,omitempty"` //alias for both groupby and orderby, cannot be used together - GroupBy string `json:"group_by"` - OrderBy string `json:"order_by"` + GroupBy *QueryResourceAggregation `json:"group_by,omitempty"` + OrderBy *QueryResourceAggregation `json:"order_by,omitempty"` +} + +type QueryResourceAggregation struct { + Field string `json:"field"` + Function string `json:"fn"` } func (q *QueryResource) Validate() error { @@ -38,26 +43,44 @@ func (q *QueryResource) Validate() error { if len(q.Select) > 0 { return fmt.Errorf("cannot use 'select' and 'aggregations' together") } - if len(q.Aggregations.CountBy) > 0 { - if len(q.Aggregations.GroupBy) > 0 { + + if q.Aggregations.CountBy != nil { + if len(q.Aggregations.CountBy.Field) == 0 { + return fmt.Errorf("if 'count_by' is present, field must be populated") + } + if strings.Contains(q.Aggregations.CountBy.Field, " ") { + return fmt.Errorf("count_by cannot have spaces (or aliases)") + } + } + if q.Aggregations.GroupBy != nil { + if len(q.Aggregations.GroupBy.Field) == 0 { + return fmt.Errorf("if 'group_by' is present, field must be populated") + } + if strings.Contains(q.Aggregations.GroupBy.Field, " ") { + return fmt.Errorf("group_by cannot have spaces (or aliases)") + } + } + if q.Aggregations.OrderBy != nil { + if len(q.Aggregations.OrderBy.Field) == 0 { + return fmt.Errorf("if 'order_by' is present, field must be populated") + } + if strings.Contains(q.Aggregations.OrderBy.Field, " ") { + return fmt.Errorf("order_by cannot have spaces (or aliases)") + } + } + + if q.Aggregations.CountBy != nil { + if q.Aggregations.GroupBy != nil { return fmt.Errorf("cannot use 'count_by' and 'group_by' together") } - if len(q.Aggregations.OrderBy) > 0 { + if q.Aggregations.OrderBy != nil { return fmt.Errorf("cannot use 'count_by' and 'order_by' together") } } - if len(q.Aggregations.CountBy) == 0 && len(q.Aggregations.OrderBy) == 0 && len(q.Aggregations.GroupBy) == 0 { + if q.Aggregations.CountBy == nil && q.Aggregations.OrderBy == nil && q.Aggregations.GroupBy == nil { return fmt.Errorf("aggregations must have at least one of 'count_by', 'group_by', or 'order_by'") } - if strings.Contains(q.Aggregations.CountBy, " ") { - return fmt.Errorf("count_by cannot have spaces (or aliases)") - } - if strings.Contains(q.Aggregations.GroupBy, " ") { - return fmt.Errorf("group_by cannot have spaces (or aliases)") - } - if strings.Contains(q.Aggregations.OrderBy, " ") { - return fmt.Errorf("order_by cannot have spaces (or aliases)") - } + } if q.Limit != nil && *q.Limit < 0 { diff --git a/backend/pkg/models/query_resource_test.go b/backend/pkg/models/query_resource_test.go index 90a1d90e..a75806e1 100644 --- a/backend/pkg/models/query_resource_test.go +++ b/backend/pkg/models/query_resource_test.go @@ -13,15 +13,18 @@ func TestQueryResource_Validate(t *testing.T) { }{ {QueryResource{Use: "test"}, "'use' is not supported yet", true}, {QueryResource{}, "'from' is required", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test"}}, "", false}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: ""}}}, "if 'count_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: ""}}}, "if 'group_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: ""}}}, "if 'order_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}}}, "", false}, {QueryResource{Select: []string{"test"}, From: "test", Aggregations: &QueryResourceAggregations{}}, "cannot use 'select' and 'aggregations' together", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", GroupBy: "test"}}, "cannot use 'count_by' and 'group_by' together", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", OrderBy: "test"}}, "cannot use 'count_by' and 'order_by' together", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, GroupBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'group_by' together", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, OrderBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'order_by' together", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{}}, "aggregations must have at least one of 'count_by', 'group_by', or 'order_by'", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property"}}, "", false}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property as HELLO"}}, "count_by cannot have spaces (or aliases)", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: "test:property as HELLO"}}, "group_by cannot have spaces (or aliases)", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: "test:property as HELLO"}}, "order_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property"}}}, "", false}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "count_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "group_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "order_by cannot have spaces (or aliases)", true}, } //test && assert diff --git a/backend/pkg/web/handler/dashboard/default.json b/backend/pkg/web/handler/dashboard/default.json index da885131..0f4df93c 100644 --- a/backend/pkg/web/handler/dashboard/default.json +++ b/backend/pkg/web/handler/dashboard/default.json @@ -132,7 +132,7 @@ "from": "Observation", "where": {}, "aggregations":{ - "count_by": "code:code" + "count_by": {"field": "code:code" } } } }], @@ -158,7 +158,7 @@ "from": "Immunization", "where": {}, "aggregations":{ - "count_by": "*" + "count_by": {"field": "*" } } } }, @@ -171,7 +171,7 @@ "from": "Claim", "where": {}, "aggregations":{ - "count_by": "*" + "count_by": {"field": "*" } } } }],