mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-09-18 19:34:32 +02:00
Add insertonly test
Moreover, this fixes the test for writeonly tokens, which used to create a table in a db, and then try querying from a separate db, which ended up not testing what it was supposed to at all. I also changed the way mutations are guarded (the end result is pretty much the same)
This commit is contained in:
parent
58e3f0bb7b
commit
8b3b1bbe37
3 changed files with 150 additions and 117 deletions
|
@ -36,7 +36,6 @@ import Protolude (
|
||||||
(&),
|
(&),
|
||||||
(&&),
|
(&&),
|
||||||
(.),
|
(.),
|
||||||
(<$>),
|
|
||||||
(<&>),
|
(<&>),
|
||||||
(<=),
|
(<=),
|
||||||
(>),
|
(>),
|
||||||
|
@ -814,6 +813,9 @@ queryType connection accessMode dbId tables = do
|
||||||
, Introspection.typeNameResolver
|
, Introspection.typeNameResolver
|
||||||
, resolvers
|
, resolvers
|
||||||
]
|
]
|
||||||
|
-- TODO: is it better to wrap the resolvers here,
|
||||||
|
-- or to just return an empty list of resolvers
|
||||||
|
-- when given a token that cannot read?
|
||||||
<&> wrapResolver requireRead
|
<&> wrapResolver requireRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1074,94 +1076,73 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
||||||
numOfChanges <- SS.changes connection
|
numOfChanges <- SS.changes connection
|
||||||
mutationByPKResponse table numOfChanges $ P.head deletedRows
|
mutationByPKResponse table numOfChanges $ P.head deletedRows
|
||||||
|
|
||||||
getMutationResolvers :: IO (HashMap.HashMap Text (Resolver IO))
|
getInsertTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
||||||
getMutationResolvers = do
|
getInsertTableTuple table =
|
||||||
let
|
makeResolver
|
||||||
getInsertTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
(Introspection.tableInsertField accessMode table)
|
||||||
getInsertTableTuple table =
|
(executeDbInserts table)
|
||||||
makeResolver
|
|
||||||
(Introspection.tableInsertField accessMode table)
|
|
||||||
(executeDbInserts table)
|
|
||||||
|
|
||||||
getUpdateTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
getUpdateTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
||||||
getUpdateTableTuple table =
|
getUpdateTableTuple table =
|
||||||
makeResolver
|
makeResolver
|
||||||
(Introspection.tableUpdateField accessMode table)
|
(Introspection.tableUpdateField accessMode table)
|
||||||
(executeDbUpdates table)
|
(executeDbUpdates table)
|
||||||
|
|
||||||
getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
||||||
getUpdateByPKTableTuple table =
|
getUpdateByPKTableTuple table =
|
||||||
P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $
|
P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $
|
||||||
\field -> makeResolver field (executeDbUpdatesByPK table)
|
\field -> makeResolver field (executeDbUpdatesByPK table)
|
||||||
|
|
||||||
getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
||||||
getDeleteTableTuple table =
|
getDeleteTableTuple table =
|
||||||
makeResolver
|
makeResolver
|
||||||
(Introspection.tableDeleteField accessMode table)
|
(Introspection.tableDeleteField accessMode table)
|
||||||
(executeDbDeletions table)
|
(executeDbDeletions table)
|
||||||
|
|
||||||
getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
||||||
getDeleteByPKTableTuple table =
|
getDeleteByPKTableTuple table =
|
||||||
P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $
|
P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $
|
||||||
\field -> makeResolver field (executeDbDeletionsByPK table)
|
\field -> makeResolver field (executeDbDeletionsByPK table)
|
||||||
|
|
||||||
tablesWithoutViews :: [TableEntry]
|
tablesWithoutViews :: [TableEntry]
|
||||||
tablesWithoutViews =
|
tablesWithoutViews =
|
||||||
List.filter
|
List.filter
|
||||||
(\table -> table.object_type == Table)
|
(\table -> table.object_type == Table)
|
||||||
tables
|
tables
|
||||||
|
|
||||||
insertTuples <-
|
insertTuples <-
|
||||||
P.fold
|
P.fold
|
||||||
[ P.for tablesWithoutViews getInsertTableTuple
|
[ P.for tablesWithoutViews getInsertTableTuple
|
||||||
]
|
]
|
||||||
|
|
||||||
writeTuples <-
|
writeTuples <-
|
||||||
P.fold
|
P.fold
|
||||||
[ P.for tablesWithoutViews getUpdateTableTuple
|
[ P.for tablesWithoutViews getUpdateTableTuple
|
||||||
, P.for tablesWithoutViews getDeleteTableTuple
|
, P.for tablesWithoutViews getDeleteTableTuple
|
||||||
, P.for tablesWithoutViews getUpdateByPKTableTuple
|
, P.for tablesWithoutViews getUpdateByPKTableTuple
|
||||||
<&> P.catMaybes
|
<&> P.catMaybes
|
||||||
, P.for tablesWithoutViews getDeleteByPKTableTuple
|
, P.for tablesWithoutViews getDeleteByPKTableTuple
|
||||||
<&> P.catMaybes
|
<&> P.catMaybes
|
||||||
]
|
]
|
||||||
|
|
||||||
let
|
let
|
||||||
requireWrite :: Out.Resolve IO -> Out.Resolve IO
|
insertResolvers =
|
||||||
requireWrite resolve = do
|
if canInsert accessMode
|
||||||
when (P.not $ canWrite accessMode) $ do
|
then HashMap.fromList insertTuples
|
||||||
throw $
|
else mempty
|
||||||
ResolverException $
|
|
||||||
userError "Cannot write field using the provided token"
|
|
||||||
resolve
|
|
||||||
|
|
||||||
requireInsert :: Out.Resolve IO -> Out.Resolve IO
|
writeResolvers =
|
||||||
requireInsert resolve = do
|
if canWrite accessMode
|
||||||
when (P.not $ canInsert accessMode) $ do
|
then HashMap.fromList writeTuples
|
||||||
throw $
|
else mempty
|
||||||
ResolverException $
|
|
||||||
userError "Cannot insert entries using the provided token"
|
|
||||||
resolve
|
|
||||||
|
|
||||||
insertResolvers =
|
pure
|
||||||
HashMap.fromList insertTuples
|
$ Just
|
||||||
<&> wrapResolver requireInsert
|
$ Out.ObjectType
|
||||||
|
"Mutation"
|
||||||
writeResolvers =
|
Nothing
|
||||||
HashMap.fromList writeTuples
|
[]
|
||||||
<&> wrapResolver requireWrite
|
$ insertResolvers <> writeResolvers
|
||||||
|
|
||||||
pure $ insertResolvers <> writeResolvers
|
|
||||||
|
|
||||||
if canWrite accessMode
|
|
||||||
then
|
|
||||||
Just
|
|
||||||
. Out.ObjectType
|
|
||||||
"Mutation"
|
|
||||||
Nothing
|
|
||||||
[]
|
|
||||||
<$> getMutationResolvers
|
|
||||||
else pure Nothing
|
|
||||||
|
|
||||||
|
|
||||||
-- | Automatically generated schema derived from the SQLite database
|
-- | Automatically generated schema derived from the SQLite database
|
||||||
|
|
|
@ -24,7 +24,7 @@ import System.FilePath ((</>))
|
||||||
import Test.Hspec (Spec, describe, it, shouldBe)
|
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||||
|
|
||||||
import AirGQL.GraphQL (getDerivedSchema)
|
import AirGQL.GraphQL (getDerivedSchema)
|
||||||
import AirGQL.Lib (getEnrichedTables, writeOnly)
|
import AirGQL.Lib (getEnrichedTables, insertOnly, writeOnly)
|
||||||
import AirGQL.Raw (raw)
|
import AirGQL.Raw (raw)
|
||||||
import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf)
|
import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf)
|
||||||
import AirGQL.Utils (withRetryConn)
|
import AirGQL.Utils (withRetryConn)
|
||||||
|
@ -181,7 +181,7 @@ main = void $ do
|
||||||
"data": null,
|
"data": null,
|
||||||
"errors": [{
|
"errors": [{
|
||||||
"locations": [{ "column":3, "line":2 }],
|
"locations": [{ "column":3, "line":2 }],
|
||||||
"message": "user error (Cannot read field using writeonly access code)",
|
"message": "user error (Cannot read field using the provided token)",
|
||||||
"path": ["__schema"]
|
"path": ["__schema"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -644,41 +644,88 @@ main = void $ do
|
||||||
)
|
)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
let
|
let
|
||||||
query :: Text
|
query :: Text
|
||||||
query =
|
query =
|
||||||
[gql|
|
[gql|
|
||||||
mutation items {
|
mutation items {
|
||||||
update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) {
|
update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) {
|
||||||
returning { id }
|
returning { id }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|]
|
|
||||||
|
|
||||||
expected =
|
|
||||||
rmSpaces
|
|
||||||
[raw|
|
|
||||||
{
|
|
||||||
"data": null,
|
|
||||||
"errors": [{
|
|
||||||
"locations": [{ "column":3, "line":2 }],
|
|
||||||
"message": "Cannot query field \"update_items\" on type \"Mutation\"."
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
schema <- withRetryConn dbPath $ \conn -> do
|
expected =
|
||||||
|
rmSpaces
|
||||||
|
[raw|
|
||||||
|
{
|
||||||
|
"data": null,
|
||||||
|
"errors": [{
|
||||||
|
"locations": [{ "column":5, "line":3 }],
|
||||||
|
"message": "Cannot query field \"returning\" on type \"items_mutation_response\"."
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
Right tables <- getEnrichedTables conn
|
Right tables <- getEnrichedTables conn
|
||||||
getDerivedSchema
|
schema <-
|
||||||
defaultSchemaConf{accessMode = writeOnly}
|
getDerivedSchema
|
||||||
|
defaultSchemaConf{accessMode = writeOnly}
|
||||||
|
conn
|
||||||
|
fixtureDbId
|
||||||
|
tables
|
||||||
|
|
||||||
|
Right response <-
|
||||||
|
graphql schema Nothing mempty query
|
||||||
|
|
||||||
|
Ae.encode response `shouldBe` expected
|
||||||
|
|
||||||
|
it "doesn't allow insertonly tokens to update data" $ do
|
||||||
|
let dbName = "no-insertonly-return.db"
|
||||||
|
withTestDbConn dbName $ \conn -> do
|
||||||
|
SS.execute_
|
||||||
conn
|
conn
|
||||||
fixtureDbId
|
[sql|
|
||||||
tables
|
CREATE TABLE items (
|
||||||
|
id INTEGER PRIMARY KEY
|
||||||
|
)
|
||||||
|
|]
|
||||||
|
|
||||||
Right response <-
|
let
|
||||||
graphql schema Nothing mempty query
|
query :: Text
|
||||||
|
query =
|
||||||
|
[gql|
|
||||||
|
mutation items {
|
||||||
|
update_items_by_pk(id: 0, set: { id: 0 }) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
Ae.encode response `shouldBe` expected
|
expected =
|
||||||
|
rmSpaces
|
||||||
|
[raw|
|
||||||
|
{
|
||||||
|
"data": null,
|
||||||
|
"errors": [{
|
||||||
|
"locations": [{ "column":3, "line":2 }],
|
||||||
|
"message": "Cannot query field \"update_items_by_pk\" on type \"Mutation\"."
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
|
Right tables <- getEnrichedTables conn
|
||||||
|
schema <-
|
||||||
|
getDerivedSchema
|
||||||
|
defaultSchemaConf{accessMode = insertOnly}
|
||||||
|
conn
|
||||||
|
fixtureDbId
|
||||||
|
tables
|
||||||
|
|
||||||
|
Right response <-
|
||||||
|
graphql schema Nothing mempty query
|
||||||
|
|
||||||
|
Ae.encode response `shouldBe` expected
|
||||||
|
|
||||||
describe "Naming conflicts" $ do
|
describe "Naming conflicts" $ do
|
||||||
it "appends _ at the end of queries to avoid conflicts with table names" $ do
|
it "appends _ at the end of queries to avoid conflicts with table names" $ do
|
||||||
|
@ -747,32 +794,32 @@ main = void $ do
|
||||||
"__schema": {
|
"__schema": {
|
||||||
"mutationType": {
|
"mutationType": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"name": "insert_foo",
|
"name": "insert_foo",
|
||||||
"args": [
|
"args": [
|
||||||
{ "name": "objects" },
|
{ "name": "objects" },
|
||||||
{ "name": "on_conflict" }
|
{ "name": "on_conflict" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "update_foo",
|
"name": "update_foo",
|
||||||
"args": [
|
"args": [
|
||||||
{ "name": "set" },
|
{ "name": "set" },
|
||||||
{ "name": "filter" }
|
{ "name": "filter" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "update_foo_by_pk",
|
"name": "update_foo_by_pk",
|
||||||
"args": [
|
"args": [
|
||||||
{ "name": "set" },
|
{ "name": "set" },
|
||||||
{ "name": "set_" }
|
{ "name": "set_" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "delete_foo",
|
"name": "delete_foo",
|
||||||
"args": [{ "name": "filter" }]
|
"args": [{ "name": "filter" }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "delete_foo_by_pk",
|
"name": "delete_foo_by_pk",
|
||||||
"args": [{ "name": "set" }]
|
"args": [{ "name": "set" }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,11 @@ import System.FilePath ((</>))
|
||||||
import Test.Hspec (Spec, describe, it, shouldBe)
|
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||||
|
|
||||||
import AirGQL.GraphQL (getDerivedSchema)
|
import AirGQL.GraphQL (getDerivedSchema)
|
||||||
import AirGQL.Lib (SQLPost (SQLPost, query), getEnrichedTables)
|
import AirGQL.Lib (SQLPost (SQLPost, query), getEnrichedTables, insertOnly)
|
||||||
import AirGQL.Raw (raw)
|
import AirGQL.Raw (raw)
|
||||||
import AirGQL.Servant.SqlQuery (sqlQueryPostHandler)
|
import AirGQL.Servant.SqlQuery (sqlQueryPostHandler)
|
||||||
import AirGQL.Types.PragmaConf qualified as PragmaConf
|
import AirGQL.Types.PragmaConf qualified as PragmaConf
|
||||||
import AirGQL.Types.SchemaConf (defaultSchemaConf)
|
import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf)
|
||||||
import AirGQL.Types.SqlQueryPostResult (
|
import AirGQL.Types.SqlQueryPostResult (
|
||||||
SqlQueryPostResult (rows),
|
SqlQueryPostResult (rows),
|
||||||
)
|
)
|
||||||
|
@ -75,7 +75,12 @@ main = void $ do
|
||||||
|
|
||||||
conn <- SS.open dbPath
|
conn <- SS.open dbPath
|
||||||
Right tables <- getEnrichedTables conn
|
Right tables <- getEnrichedTables conn
|
||||||
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
|
schema <-
|
||||||
|
getDerivedSchema
|
||||||
|
defaultSchemaConf{accessMode = insertOnly}
|
||||||
|
conn
|
||||||
|
fixtureDbId
|
||||||
|
tables
|
||||||
Right result <- graphql schema Nothing mempty query
|
Right result <- graphql schema Nothing mempty query
|
||||||
|
|
||||||
Ae.encode result `shouldBe` expected
|
Ae.encode result `shouldBe` expected
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue