mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
eab4f75212
This introduces an `ErrorMessage` newtype which wraps `Text` in a manner which is designed to be easy to construct, and difficult to deconstruct. It provides functionality similar to `Data.Text.Extended`, but designed _only_ for error messages. Error messages are constructed through `fromString`, concatenation, or the `toErrorValue` function, which is designed to be overridden for all meaningful domain types that might show up in an error message. Notably, there are not and should never be instances of `ToErrorValue` for `String`, `Text`, `Int`, etc. This is so that we correctly represent the value in a way that is specific to its type. For example, all `Name` values (from the _graphql-parser-hs_ library) are single-quoted now; no exceptions. I have mostly had to add `instance ToErrorValue` for various backend types (and also add newtypes where necessary). Some of these are not strictly necessary for this changeset, as I had bigger aspirations when I started. These aspirations have been tempered by trying and failing twice. As such, in this changeset, I have started by introducing this type to the `parseError` and `parseErrorWith` functions. In the future, I would like to extend this to the `QErr` record and the various `throwError` functions, but this is a much larger task and should probably be done in stages. For now, `toErrorMessage` and `fromErrorMessage` are provided for conversion to and from `Text`, but the intent is to stop exporting these once all error messages are converted to the new type. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5018 GitOrigin-RevId: 84b37e238992e4312255a87ca44f41af65e2d89a
459 lines
10 KiB
Haskell
459 lines
10 KiB
Haskell
{-# LANGUAGE QuasiQuotes #-}
|
|
|
|
-- | All tests related to computed fields in a BigQuery source
|
|
module Test.BigQuery.ComputedFieldSpec (spec) where
|
|
|
|
import Data.Text qualified as T
|
|
import Harness.Backend.BigQuery qualified as BigQuery
|
|
import Harness.Constants qualified as Constants
|
|
import Harness.Exceptions (finally)
|
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
|
import Harness.Quoter.Graphql (graphql)
|
|
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
|
import Harness.Test.Context qualified as Context
|
|
import Harness.Test.Schema (Table (..), table)
|
|
import Harness.Test.Schema qualified as Schema
|
|
import Harness.TestEnvironment (TestEnvironment)
|
|
import Test.Hspec (SpecWith, it)
|
|
import Prelude
|
|
|
|
-- ** Preamble
|
|
|
|
spec :: SpecWith TestEnvironment
|
|
spec =
|
|
Context.run
|
|
[ Context.Context
|
|
{ name = Context.Backend Context.BigQuery,
|
|
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
|
|
setup = bigquerySetup,
|
|
teardown = bigqueryTeardown,
|
|
customOptions = Nothing
|
|
}
|
|
]
|
|
tests
|
|
|
|
-- ** Setup and teardown
|
|
|
|
bigquerySetup :: (TestEnvironment, ()) -> IO ()
|
|
bigquerySetup (testEnv, ()) = do
|
|
BigQuery.setup [authorTable, articleTable] (testEnv, ())
|
|
|
|
-- Create functions in BigQuery
|
|
BigQuery.runSql_ createFunctionsSQL
|
|
|
|
-- Add computed fields and define select permissions
|
|
let dataset = Constants.bigqueryDataset
|
|
GraphqlEngine.postMetadata_
|
|
testEnv
|
|
[yaml|
|
|
type: bulk
|
|
args:
|
|
- type: bigquery_add_computed_field
|
|
args:
|
|
source: bigquery
|
|
name: search_articles_1
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
definition:
|
|
function:
|
|
dataset: *dataset
|
|
name: fetch_articles_returns_table
|
|
argument_mapping:
|
|
a_id: id
|
|
|
|
- type: bigquery_add_computed_field
|
|
args:
|
|
source: bigquery
|
|
name: search_articles_2
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
definition:
|
|
function:
|
|
dataset: *dataset
|
|
name: fetch_articles
|
|
argument_mapping:
|
|
a_id: id
|
|
return_table:
|
|
name: article
|
|
dataset: *dataset
|
|
|
|
# Role user_1 has select permissions on author and article tables.
|
|
# user_1 can query search_articles_1 computed field.
|
|
- type: bigquery_create_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
role: user_1
|
|
permission:
|
|
columns: '*'
|
|
filter: {}
|
|
computed_fields:
|
|
- search_articles_1
|
|
|
|
- type: bigquery_create_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: article
|
|
role: user_1
|
|
permission:
|
|
columns: '*'
|
|
filter: {}
|
|
|
|
# Role user_2 has select permissions only on author table.
|
|
- type: bigquery_create_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
role: user_2
|
|
permission:
|
|
columns: '*'
|
|
filter: {}
|
|
|]
|
|
|
|
bigqueryTeardown :: (TestEnvironment, ()) -> IO ()
|
|
bigqueryTeardown (testEnv, ()) = do
|
|
-- Drop permissions and computed fields metadata
|
|
let dataset = Constants.bigqueryDataset
|
|
dropComputedFieldsYaml =
|
|
[yaml|
|
|
type: bulk
|
|
args:
|
|
- type: bigquery_drop_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
role: user_1
|
|
|
|
- type: bigquery_drop_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: article
|
|
role: user_1
|
|
|
|
- type: bigquery_drop_select_permission
|
|
args:
|
|
source: bigquery
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
role: user_2
|
|
|
|
- type: bigquery_drop_computed_field
|
|
args:
|
|
source: bigquery
|
|
name: search_articles_1
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
|
|
- type: bigquery_drop_computed_field
|
|
args:
|
|
source: bigquery
|
|
name: search_articles_2
|
|
table:
|
|
dataset: *dataset
|
|
name: author
|
|
|]
|
|
finally
|
|
(GraphqlEngine.postMetadata_ testEnv dropComputedFieldsYaml)
|
|
( finally
|
|
-- Drop functions in BigQuery database
|
|
(BigQuery.runSql_ dropFunctionsSQL)
|
|
-- Teardown schema
|
|
(BigQuery.teardown [authorTable, articleTable] (testEnv, ()))
|
|
)
|
|
|
|
authorTable :: Table
|
|
authorTable =
|
|
(table "author")
|
|
{ tableColumns =
|
|
[ Schema.column "id" Schema.TInt,
|
|
Schema.column "name" Schema.TStr
|
|
],
|
|
tablePrimaryKey = ["id"],
|
|
tableData =
|
|
[ [ Schema.VInt 1,
|
|
Schema.VStr "Author 1"
|
|
],
|
|
[ Schema.VInt 2,
|
|
Schema.VStr "Author 2"
|
|
]
|
|
]
|
|
}
|
|
|
|
articleTable :: Table
|
|
articleTable =
|
|
(table "article")
|
|
{ tableColumns =
|
|
[ Schema.column "id" Schema.TInt,
|
|
Schema.column "title" Schema.TStr,
|
|
Schema.column "content" Schema.TStr,
|
|
Schema.column "author_id" Schema.TInt
|
|
],
|
|
tablePrimaryKey = ["id"],
|
|
tableData =
|
|
[ [ Schema.VInt 1,
|
|
Schema.VStr "Article 1 Title",
|
|
Schema.VStr "Article 1 by Author 1",
|
|
Schema.VInt 1
|
|
],
|
|
[ Schema.VInt 2,
|
|
Schema.VStr "Article 2 Title",
|
|
Schema.VStr "Article 2 by Author 2",
|
|
Schema.VInt 2
|
|
],
|
|
[ Schema.VInt 3,
|
|
Schema.VStr "Article 3 Title",
|
|
Schema.VStr "Article 3 by Author 2, has search keyword",
|
|
Schema.VInt 2
|
|
]
|
|
]
|
|
}
|
|
|
|
fetch_articles_returns_table :: T.Text
|
|
fetch_articles_returns_table =
|
|
T.pack Constants.bigqueryDataset <> ".fetch_articles_returns_table"
|
|
|
|
fetch_articles :: T.Text
|
|
fetch_articles =
|
|
T.pack Constants.bigqueryDataset <> ".fetch_articles"
|
|
|
|
createFunctionsSQL :: String
|
|
createFunctionsSQL =
|
|
T.unpack $
|
|
T.unwords $
|
|
[ "CREATE TABLE FUNCTION ",
|
|
fetch_articles_returns_table,
|
|
"(a_id INT64, search STRING)",
|
|
"RETURNS TABLE<id INT64, title STRING, content STRING>",
|
|
"AS (",
|
|
"SELECT t.id, t.title, t.content FROM",
|
|
articleTableSQL,
|
|
"AS t WHERE t.author_id = a_id and (t.title LIKE `search` OR t.content LIKE `search`)",
|
|
");"
|
|
]
|
|
<> [ "CREATE TABLE FUNCTION ",
|
|
fetch_articles,
|
|
"(a_id INT64, search STRING)",
|
|
"AS (",
|
|
"SELECT t.* FROM",
|
|
articleTableSQL,
|
|
"AS t WHERE t.author_id = a_id and (t.title LIKE `search` OR t.content LIKE `search`)",
|
|
");"
|
|
]
|
|
where
|
|
articleTableSQL = T.pack Constants.bigqueryDataset <> ".article"
|
|
|
|
dropFunctionsSQL :: String
|
|
dropFunctionsSQL =
|
|
T.unpack $
|
|
T.unwords $
|
|
[ "DROP TABLE FUNCTION " <> fetch_articles_returns_table <> ";",
|
|
"DROP TABLE FUNCTION " <> fetch_articles <> ";"
|
|
]
|
|
|
|
-- * Tests
|
|
|
|
tests :: Context.Options -> SpecWith TestEnvironment
|
|
tests opts = do
|
|
it "Query with computed fields" $ \testEnv ->
|
|
shouldReturnYaml
|
|
opts
|
|
( GraphqlEngine.postGraphql
|
|
testEnv
|
|
[graphql|
|
|
query {
|
|
hasura_author(order_by: {id: asc}){
|
|
id
|
|
name
|
|
search_articles_1(args: {search: "%1%"}){
|
|
id
|
|
title
|
|
content
|
|
}
|
|
search_articles_2(args: {search: "%keyword%"}){
|
|
id
|
|
title
|
|
content
|
|
author_id
|
|
}
|
|
}
|
|
}
|
|
|]
|
|
)
|
|
[yaml|
|
|
data:
|
|
hasura_author:
|
|
- id: '1'
|
|
name: Author 1
|
|
search_articles_1:
|
|
- id: '1'
|
|
title: Article 1 Title
|
|
content: Article 1 by Author 1
|
|
search_articles_2: []
|
|
- id: '2'
|
|
name: Author 2
|
|
search_articles_1: []
|
|
search_articles_2:
|
|
- id: '3'
|
|
title: Article 3 Title
|
|
content: Article 3 by Author 2, has search keyword
|
|
author_id: '2'
|
|
|]
|
|
|
|
it "Query with computed fields using limit and order_by" $ \testEnv ->
|
|
shouldReturnYaml
|
|
opts
|
|
( GraphqlEngine.postGraphql
|
|
testEnv
|
|
[graphql|
|
|
query {
|
|
hasura_author(order_by: {id: asc}){
|
|
id
|
|
name
|
|
search_articles_2(args: {search: "%by%"} limit: 1 order_by: {id: asc}){
|
|
id
|
|
title
|
|
content
|
|
author_id
|
|
}
|
|
}
|
|
}
|
|
|]
|
|
)
|
|
[yaml|
|
|
data:
|
|
hasura_author:
|
|
- id: '1'
|
|
name: Author 1
|
|
search_articles_2:
|
|
- author_id: '1'
|
|
content: Article 1 by Author 1
|
|
id: '1'
|
|
title: Article 1 Title
|
|
- id: '2'
|
|
name: Author 2
|
|
search_articles_2:
|
|
- author_id: '2'
|
|
content: Article 2 by Author 2
|
|
id: '2'
|
|
title: Article 2 Title
|
|
|]
|
|
|
|
it "Query with computed fields as user_1 role" $ \testEnv ->
|
|
shouldReturnYaml
|
|
opts
|
|
( GraphqlEngine.postGraphqlWithHeaders
|
|
testEnv
|
|
[("X-Hasura-Role", "user_1")]
|
|
[graphql|
|
|
query {
|
|
hasura_author(order_by: {id: asc}){
|
|
id
|
|
name
|
|
search_articles_1(args: {search: "%1%"}){
|
|
id
|
|
title
|
|
content
|
|
}
|
|
search_articles_2(args: {search: "%keyword%"}){
|
|
id
|
|
title
|
|
content
|
|
author_id
|
|
}
|
|
}
|
|
}
|
|
|]
|
|
)
|
|
[yaml|
|
|
data:
|
|
hasura_author:
|
|
- id: '1'
|
|
name: Author 1
|
|
search_articles_1:
|
|
- id: '1'
|
|
title: Article 1 Title
|
|
content: Article 1 by Author 1
|
|
search_articles_2: []
|
|
- id: '2'
|
|
name: Author 2
|
|
search_articles_1: []
|
|
search_articles_2:
|
|
- id: '3'
|
|
title: Article 3 Title
|
|
content: Article 3 by Author 2, has search keyword
|
|
author_id: '2'
|
|
|]
|
|
|
|
it "Query with computed field search_articles_1 as user_2 role" $ \testEnv ->
|
|
shouldReturnYaml
|
|
opts
|
|
( GraphqlEngine.postGraphqlWithHeaders
|
|
testEnv
|
|
[("X-Hasura-Role", "user_2")]
|
|
[graphql|
|
|
query {
|
|
hasura_author(order_by: {id: asc}){
|
|
id
|
|
name
|
|
search_articles_1(args: {search: "%1%"}){
|
|
id
|
|
title
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|]
|
|
)
|
|
[yaml|
|
|
errors:
|
|
- extensions:
|
|
path: "$.selectionSet.hasura_author.selectionSet.search_articles_1"
|
|
code: validation-failed
|
|
message: |-
|
|
field 'search_articles_1' not found in type: 'hasura_author'
|
|
|]
|
|
|
|
it "Query with computed field search_articles_2 as user_2 role" $ \testEnv ->
|
|
shouldReturnYaml
|
|
opts
|
|
( GraphqlEngine.postGraphqlWithHeaders
|
|
testEnv
|
|
[("X-Hasura-Role", "user_2")]
|
|
[graphql|
|
|
query {
|
|
hasura_author(order_by: {id: asc}){
|
|
id
|
|
name
|
|
search_articles_2(args: {search: "%keyword%"}){
|
|
id
|
|
title
|
|
content
|
|
author_id
|
|
}
|
|
}
|
|
}
|
|
|]
|
|
)
|
|
[yaml|
|
|
errors:
|
|
- extensions:
|
|
path: "$.selectionSet.hasura_author.selectionSet.search_articles_2"
|
|
code: validation-failed
|
|
message: |-
|
|
field 'search_articles_2' not found in type: 'hasura_author'
|
|
|]
|