mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-07-08 11:09:32 +02:00
1674 lines
54 KiB
Haskell
1674 lines
54 KiB
Haskell
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
|
|
{-# HLINT ignore "Use list comprehension" #-}
|
|
{-# HLINT ignore "Replace case with maybe" #-}
|
|
|
|
module AirGQL.GraphQL (
|
|
getDerivedSchema,
|
|
queryType,
|
|
sqlDataToGQLValue,
|
|
getMutationResponse,
|
|
gqlValueToSQLData,
|
|
)
|
|
where
|
|
|
|
import Protolude (
|
|
Applicative (pure),
|
|
Bool (False, True),
|
|
Double,
|
|
Either (Left, Right),
|
|
Eq ((==)),
|
|
IO,
|
|
Int,
|
|
Integer,
|
|
Maybe (Just, Nothing),
|
|
MonadIO (liftIO),
|
|
MonadReader (ask),
|
|
Monoid (mempty),
|
|
ReaderT,
|
|
Semigroup ((<>)),
|
|
Text,
|
|
Traversable (sequence),
|
|
fromIntegral,
|
|
fromMaybe,
|
|
notElem,
|
|
otherwise,
|
|
show,
|
|
when,
|
|
($),
|
|
(&),
|
|
(&&),
|
|
(.),
|
|
(<$>),
|
|
(<&>),
|
|
(<=),
|
|
(>),
|
|
(>=),
|
|
)
|
|
import Protolude qualified as P
|
|
|
|
import Control.Exception (throw)
|
|
import Control.Monad.Catch (catchAll)
|
|
import Data.Aeson (object, (.=))
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
import Data.List (nub)
|
|
import Data.Ord (Ord (min))
|
|
import Data.Text (intercalate, isInfixOf, pack, toUpper)
|
|
import Data.Text qualified as T
|
|
import Database.SQLite.Simple (
|
|
Connection,
|
|
Query (Query),
|
|
SQLData (SQLBlob, SQLFloat, SQLInteger, SQLNull, SQLText),
|
|
changes,
|
|
execute_,
|
|
query,
|
|
query_,
|
|
)
|
|
import Database.SQLite.Simple qualified as SS
|
|
import DoubleXEncoding (doubleXDecode, doubleXEncodeGql)
|
|
import GHC.IO.Exception (userError)
|
|
import Language.GraphQL.AST.Document (Name)
|
|
import Language.GraphQL.Error (ResolverException (ResolverException))
|
|
import Language.GraphQL.Type as GQL (
|
|
Arguments (Arguments),
|
|
EnumType (EnumType),
|
|
EnumValue (EnumValue),
|
|
InputField (InputField),
|
|
Resolver (EventStreamResolver, ValueResolver),
|
|
ScalarType,
|
|
Schema,
|
|
Value (Boolean, Enum, Float, Int, List, Null, Object, String),
|
|
boolean,
|
|
float,
|
|
int,
|
|
schema,
|
|
string,
|
|
)
|
|
import Language.GraphQL.Type.In (
|
|
InputObjectType (InputObjectType),
|
|
Type (NamedInputObjectType),
|
|
)
|
|
import Language.GraphQL.Type.In qualified as In
|
|
import Language.GraphQL.Type.Out qualified as Out
|
|
import Numeric (showFFloat)
|
|
|
|
import AirGQL.Config (
|
|
maxGraphqlResultCount,
|
|
)
|
|
import AirGQL.GQLWrapper (
|
|
InArgument (InArgument, argDescMb, argType, valueMb),
|
|
OutField (OutField, arguments, descriptionMb, fieldType),
|
|
inArgumentToArgument,
|
|
outFieldToField,
|
|
)
|
|
import AirGQL.Introspection (getSchemaResolver, typeNameResolver)
|
|
import AirGQL.Lib (
|
|
AccessMode (ReadAndWrite, ReadOnly, WriteOnly),
|
|
ColumnEntry (column_name, datatype, datatype_gql),
|
|
GqlTypeName (root),
|
|
TableEntryRaw (name),
|
|
column_name_gql,
|
|
getColumns,
|
|
)
|
|
import AirGQL.Types.OutObjectType (
|
|
OutObjectType (OutObjectType, descriptionMb, fields, interfaceTypes, name),
|
|
outObjectTypeToObjectType,
|
|
)
|
|
import AirGQL.Types.PragmaConf (getSQLitePragmas)
|
|
import AirGQL.Types.SchemaConf (
|
|
SchemaConf (accessMode, maxRowsPerTable, pragmaConf),
|
|
)
|
|
import AirGQL.Types.Utils (encodeToText)
|
|
import AirGQL.Utils (colToFileUrl, collectErrorList, quoteKeyword, quoteText)
|
|
|
|
|
|
typeNameToScalarType :: Maybe GqlTypeName -> ScalarType
|
|
typeNameToScalarType Nothing = string
|
|
typeNameToScalarType (Just typeName) =
|
|
case typeName.root of
|
|
"Int" -> int
|
|
"Float" -> float
|
|
"String" -> string
|
|
"Boolean" -> boolean
|
|
_ -> string
|
|
|
|
|
|
-- | Prevent numbers of being shown with exponents (0.01 instead of 1e-2)
|
|
showFullPrecision :: Double -> Text
|
|
showFullPrecision x =
|
|
pack $ showFFloat Nothing x ""
|
|
|
|
|
|
showGqlValue :: Value -> Text
|
|
showGqlValue = \case
|
|
String str -> str
|
|
Int integer -> show integer
|
|
Float double -> showFullPrecision double
|
|
Boolean bool -> show bool
|
|
Enum text -> text
|
|
List list -> "[" <> T.intercalate ", " (list <&> showGqlValue) <> "]"
|
|
Object obj -> show $ Object obj
|
|
Null -> "null"
|
|
|
|
|
|
gqlValueToSQLText :: Value -> Text
|
|
gqlValueToSQLText = \case
|
|
String str -> quoteText str
|
|
Int integer -> show integer
|
|
Float double -> showFullPrecision double
|
|
Boolean bool -> T.toUpper $ show bool
|
|
Enum text -> text
|
|
List list ->
|
|
quoteText $
|
|
"[" <> T.intercalate ", " (list <&> showGqlValue) <> "]"
|
|
Object obj -> quoteText $ show $ Object obj
|
|
Null -> "NULL"
|
|
|
|
|
|
-- TODO: Add Support for GraphQL's type "ID"
|
|
|
|
-- | Convert any GraphQL value to a nullable String
|
|
gqlValueToNullableString :: Value -> Value
|
|
gqlValueToNullableString value =
|
|
case value of
|
|
String text -> String text
|
|
Null -> Null
|
|
val -> String $ showGqlValue val
|
|
|
|
|
|
colNamesWithValResolver :: [ColumnEntry] -> [(Text, Resolver IO)]
|
|
colNamesWithValResolver columnEntries =
|
|
columnEntries <&> \colEntry ->
|
|
let
|
|
fieldToResolve =
|
|
Out.Field
|
|
(Just colEntry.column_name_gql)
|
|
( Out.NamedScalarType $
|
|
typeNameToScalarType
|
|
colEntry.datatype_gql
|
|
)
|
|
mempty
|
|
|
|
resolvedValue = do
|
|
context <- ask
|
|
pure $ case context.values of
|
|
Object obj ->
|
|
case obj & HashMap.lookup colEntry.column_name_gql of
|
|
Nothing -> String "Error: Field does not exist"
|
|
Just val ->
|
|
case colEntry.datatype of
|
|
-- Coerce value to nullable String
|
|
-- if no datatype is set.
|
|
-- This happens for columns in views.
|
|
"" -> gqlValueToNullableString val
|
|
_ -> val
|
|
_ -> String "Error: Value could not be retrieved"
|
|
in
|
|
( colEntry.column_name_gql
|
|
, ValueResolver fieldToResolve resolvedValue
|
|
)
|
|
|
|
|
|
buildSortClause :: [ColumnEntry] -> [(Name, Value)] -> Text
|
|
buildSortClause columnEntries orderElems =
|
|
if P.null orderElems
|
|
then
|
|
if "rowid" `P.elem` (columnEntries <&> T.toLower . AirGQL.Lib.column_name)
|
|
then "ORDER BY rowid ASC"
|
|
else ""
|
|
else
|
|
"ORDER BY "
|
|
<> ( orderElems
|
|
<&> ( \(name, value) ->
|
|
( name
|
|
, case value of
|
|
Enum "ASC" -> "ASC"
|
|
Enum "asc" -> "ASC"
|
|
Enum "DESC" -> "DESC"
|
|
Enum "desc" -> "DESC"
|
|
_ -> ""
|
|
)
|
|
)
|
|
<&> (\(name, order) -> name <> " " <> order)
|
|
& T.intercalate ", "
|
|
)
|
|
|
|
|
|
data Pagination = Pagination
|
|
{ limit :: Int
|
|
, offset :: Maybe Int
|
|
}
|
|
|
|
|
|
buildPaginationClause :: Maybe Pagination -> Text
|
|
buildPaginationClause = \case
|
|
Nothing -> ""
|
|
Just pagination ->
|
|
P.fold
|
|
[ "LIMIT "
|
|
, show (min pagination.limit maxGraphqlResultCount)
|
|
, case pagination.offset of
|
|
Nothing -> ""
|
|
Just offset -> "\nOFFSET " <> show offset
|
|
]
|
|
|
|
|
|
getColNamesQuoted :: [ColumnEntry] -> [Text]
|
|
getColNamesQuoted columnEntries =
|
|
columnEntries
|
|
<&> ( \col ->
|
|
( if "BLOB" `T.isPrefixOf` col.datatype
|
|
then
|
|
"IIF("
|
|
<> quoteKeyword col.column_name
|
|
<> " IS NOT NULL, rowid, NULL)"
|
|
<> " AS "
|
|
<> quoteKeyword col.column_name
|
|
else quoteKeyword col.column_name
|
|
)
|
|
)
|
|
|
|
|
|
opAndValToSql :: HashMap.HashMap Text Value -> [Text]
|
|
opAndValToSql operatorAndValue =
|
|
case HashMap.toList operatorAndValue of
|
|
[("eq", value)] ->
|
|
pure $
|
|
if value == Null
|
|
then " IS NULL"
|
|
else " == " <> gqlValueToSQLText value
|
|
[("neq", value)] ->
|
|
if value == Null
|
|
then pure " IS NOT NULL"
|
|
else
|
|
[ " != " <> gqlValueToSQLText value
|
|
, " IS NULL"
|
|
]
|
|
[("in", List values)] ->
|
|
let listValues = values <&> gqlValueToSQLText & intercalate ","
|
|
in [" IN (" <> listValues <> ")"]
|
|
[("nin", List values)] ->
|
|
let listValues = values <&> gqlValueToSQLText & intercalate ","
|
|
in [" NOT IN (" <> listValues <> ")"]
|
|
<> if P.elem Null values
|
|
then []
|
|
else [" IS NULL"]
|
|
[("gt", value)] -> [" > " <> gqlValueToSQLText value]
|
|
[("gte", value)] -> [" >= " <> gqlValueToSQLText value]
|
|
[("lt", value)] -> [" < " <> gqlValueToSQLText value]
|
|
[("lte", value)] -> [" <= " <> gqlValueToSQLText value]
|
|
[("like", value)] -> [" like " <> gqlValueToSQLText value]
|
|
[("ilike", value)] -> [" like " <> gqlValueToSQLText value]
|
|
filter -> do
|
|
throw $
|
|
userError $
|
|
"Error: Filter "
|
|
<> show filter
|
|
<> " is not yet supported"
|
|
|
|
|
|
getWhereClause :: [(Text, Value)] -> Text
|
|
getWhereClause filterElements =
|
|
if P.null filterElements
|
|
then " "
|
|
else
|
|
"WHERE "
|
|
<> ( filterElements
|
|
<&> ( \(colName, x) -> case x of
|
|
Object operatorAndValue ->
|
|
let orClauses =
|
|
opAndValToSql operatorAndValue
|
|
<&> (colName <>)
|
|
& intercalate " OR "
|
|
in "(" <> orClauses <> ")"
|
|
_ -> ""
|
|
)
|
|
& intercalate " AND "
|
|
)
|
|
|
|
|
|
setCaseInsensitive :: Connection -> [(Text, Value)] -> IO ()
|
|
setCaseInsensitive connection filterElements = do
|
|
when
|
|
( filterElements
|
|
& P.any
|
|
( \(_, value) -> case value of
|
|
Object operatorAndValue ->
|
|
case HashMap.toList operatorAndValue of
|
|
[("ilike", _)] -> True
|
|
_ -> False
|
|
_ -> False
|
|
)
|
|
)
|
|
$ do
|
|
execute_ connection "PRAGMA case_sensitive_like = False"
|
|
|
|
|
|
executeSqlQuery
|
|
:: Connection
|
|
-> Text
|
|
-> [ColumnEntry]
|
|
-> [(Text, Value)]
|
|
-> [(Text, Value)]
|
|
-> Maybe Pagination
|
|
-> IO [[SQLData]]
|
|
executeSqlQuery
|
|
connection
|
|
tableName
|
|
colEntries
|
|
filterElems
|
|
orderElems
|
|
paginationMb = do
|
|
let
|
|
sqlQuery :: Query
|
|
sqlQuery =
|
|
Query $
|
|
"SELECT "
|
|
<> intercalate ", " (getColNamesQuoted colEntries)
|
|
<> "\n"
|
|
<> "FROM "
|
|
<> quoteKeyword tableName
|
|
<> "\n"
|
|
<> getWhereClause filterElems
|
|
<> "\n"
|
|
<> buildSortClause colEntries orderElems
|
|
<> "\n"
|
|
<> buildPaginationClause paginationMb
|
|
|
|
setCaseInsensitive connection filterElems
|
|
|
|
liftIO $ query_ connection sqlQuery
|
|
|
|
|
|
colNamesWithFilterField :: Text -> [ColumnEntry] -> [(Text, InputField)]
|
|
colNamesWithFilterField tableName columnEntries =
|
|
columnEntries <&> \colEntry ->
|
|
let
|
|
inputField =
|
|
InputField
|
|
(Just $ "Filter for " <> colEntry.column_name_gql)
|
|
( NamedInputObjectType $
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_filter")
|
|
(Just "Filter object for the column")
|
|
( let theInputField =
|
|
InputField
|
|
(Just "Value to compare to")
|
|
( In.NamedScalarType $
|
|
typeNameToScalarType
|
|
colEntry.datatype_gql
|
|
)
|
|
Nothing -- Default value
|
|
listInputField =
|
|
InputField
|
|
(Just "Values to compare to")
|
|
( In.ListType $
|
|
In.NamedScalarType $
|
|
typeNameToScalarType
|
|
colEntry.datatype_gql
|
|
)
|
|
Nothing -- Default value
|
|
in HashMap.fromList
|
|
[ ("eq", theInputField)
|
|
, ("neq", theInputField)
|
|
, ("gt", theInputField)
|
|
, ("gte", theInputField)
|
|
, ("lt", theInputField)
|
|
, ("lte", theInputField)
|
|
, ("like", theInputField)
|
|
, ("ilike", theInputField)
|
|
, ("in", listInputField)
|
|
, ("nin", listInputField)
|
|
]
|
|
)
|
|
)
|
|
Nothing -- Default value
|
|
in
|
|
( colEntry.column_name_gql
|
|
, inputField
|
|
)
|
|
|
|
|
|
queryType
|
|
:: Connection
|
|
-> AccessMode
|
|
-> Text
|
|
-> [TableEntryRaw]
|
|
-> IO (Out.ObjectType IO)
|
|
queryType connection accessMode dbId tables = do
|
|
let
|
|
documentation :: Text
|
|
documentation =
|
|
"Available queries for database \"" <> dbId <> "\""
|
|
|
|
getOutField :: Text -> IO (Out.Field IO)
|
|
getOutField tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
let
|
|
colNamesWithOrderingTerm :: [(Text, InputField)]
|
|
colNamesWithOrderingTerm =
|
|
columnEntries <&> \colEntry ->
|
|
( colEntry.column_name_gql
|
|
, InputField
|
|
(Just $ "Ordering term for " <> colEntry.column_name_gql)
|
|
( In.NamedEnumType $
|
|
EnumType
|
|
"OrderingTerm"
|
|
(Just "Ordering object for the column")
|
|
( HashMap.fromList
|
|
[ ("ASC", EnumValue (Just "ASC"))
|
|
, ("asc", EnumValue (Just "ASC"))
|
|
, ("DESC", EnumValue (Just "DESC"))
|
|
, ("desc", EnumValue (Just "DESC"))
|
|
]
|
|
)
|
|
)
|
|
Nothing -- Default value
|
|
)
|
|
|
|
typeNameField :: Text -> [(Text, Resolver IO)]
|
|
typeNameField nameOfTable =
|
|
let
|
|
typeNameOutField =
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just $ "The type name of " <> nameOfTable
|
|
, fieldType = Out.NonNullScalarType string
|
|
, arguments = HashMap.empty
|
|
}
|
|
in
|
|
[
|
|
( "__typename"
|
|
, ValueResolver typeNameOutField $
|
|
pure $
|
|
String $
|
|
doubleXEncodeGql nameOfTable <> "_row"
|
|
)
|
|
]
|
|
|
|
pure $
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just $ "Provides entries from " <> tableName
|
|
, fieldType =
|
|
Out.ListType $
|
|
Out.NamedObjectType $
|
|
Out.ObjectType
|
|
tableName
|
|
(Just "short desc")
|
|
[]
|
|
( HashMap.fromList $
|
|
colNamesWithValResolver columnEntries
|
|
<> typeNameField tableName
|
|
)
|
|
, arguments =
|
|
HashMap.fromList
|
|
[
|
|
( "filter"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just "Filter objects"
|
|
, argType =
|
|
NamedInputObjectType $
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_filter")
|
|
( Just
|
|
"Filter objects for the specified columns"
|
|
)
|
|
(HashMap.fromList (colNamesWithFilterField tableName columnEntries))
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
,
|
|
( "order_by"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just "Order by the specified columns"
|
|
, argType =
|
|
In.ListType $
|
|
In.NamedInputObjectType $
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_order_by")
|
|
(Just "Options for ordering by columns")
|
|
(HashMap.fromList colNamesWithOrderingTerm)
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
,
|
|
( "limit"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb =
|
|
Just "Limit the number of returned rows."
|
|
, argType = In.NamedScalarType int
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
,
|
|
( "offset"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb =
|
|
Just
|
|
"Change the index rows \
|
|
\start being returned from"
|
|
, argType = In.NamedScalarType int
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
]
|
|
}
|
|
-- -- TODO: Use for retrieving record by primary key
|
|
-- , arguments = HashMap.fromList $ columnEntries
|
|
-- <&> (\colEntry ->
|
|
-- ( colEntry.column_name_gql :: Text
|
|
-- , inArgumentToArgument $ InArgument
|
|
-- { argDescMb = Just "Retrieve object by primary key"
|
|
-- , argType = In.NamedScalarType $
|
|
-- typeNameToScalarType $ colEntry.datatype
|
|
-- , valueMb = Nothing
|
|
-- }
|
|
-- )
|
|
-- )
|
|
|
|
getDbEntries :: Text -> Out.Resolve IO
|
|
getDbEntries tableName = do
|
|
context <- ask
|
|
colEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
rows :: [[SQLData]] <- case context.arguments of
|
|
Arguments args -> do
|
|
filterElements <- case args & HashMap.lookup "filter" of
|
|
Nothing -> pure []
|
|
Just colToFilter -> case colToFilter of
|
|
Object filterObj -> case HashMap.toList filterObj of
|
|
[] -> P.throwIO $ userError "Error: Filter must not be empty"
|
|
filterElements -> pure filterElements
|
|
_ -> pure []
|
|
|
|
orderElements :: [(Name, Value)] <-
|
|
case args & HashMap.lookup "order_by" of
|
|
Nothing -> pure []
|
|
Just colToOrder -> case colToOrder of
|
|
List objects ->
|
|
-- => [Value]
|
|
objects
|
|
-- => IO [[(Name, Value)]]
|
|
& P.traverse
|
|
( \case
|
|
Object orderObject -> case HashMap.toList orderObject of
|
|
[] -> P.throwIO $ userError "Error: Order must not be empty"
|
|
orderElements -> pure orderElements
|
|
_ -> pure [] -- Should not be reachable
|
|
)
|
|
-- => IO [(Name, Value)]
|
|
<&> P.join
|
|
_ -> pure []
|
|
|
|
limitElements :: Maybe P.Int32 <-
|
|
case args & HashMap.lookup "limit" of
|
|
Just (Int limit)
|
|
| limit >= 0 ->
|
|
pure (Just limit)
|
|
| otherwise ->
|
|
P.throwIO $
|
|
userError
|
|
"Error: limit must be positive"
|
|
_ -> pure Nothing
|
|
|
|
paginationMb :: Maybe Pagination <-
|
|
case (limitElements, args & HashMap.lookup "offset") of
|
|
(Just limit, Just (Int offset))
|
|
| offset >= 0 ->
|
|
pure $
|
|
Just $
|
|
Pagination
|
|
(fromIntegral limit)
|
|
(Just $ fromIntegral offset)
|
|
| otherwise ->
|
|
P.throwIO $ userError "Error: offset must be positive"
|
|
(Just limit, _) ->
|
|
pure $
|
|
Just $
|
|
Pagination
|
|
(fromIntegral limit)
|
|
Nothing
|
|
(Nothing, Just (Int _)) ->
|
|
P.throwIO $
|
|
userError
|
|
"Error: cannot specify offset \
|
|
\without also specifying a limit"
|
|
_ -> pure Nothing
|
|
|
|
let
|
|
countQuery :: Query
|
|
countQuery =
|
|
Query $
|
|
P.fold
|
|
[ "SELECT COUNT() FROM"
|
|
, quoteKeyword tableName
|
|
, "\n"
|
|
, getWhereClause filterElements
|
|
]
|
|
|
|
-- Will be equal `Just numRows` when the number of
|
|
-- returned rows is too large.
|
|
tooManyReturnedRows :: Maybe Int <- case paginationMb of
|
|
-- Limit doesn't seem to affect COUNT(),
|
|
-- so we consider it manually.
|
|
Just pagination
|
|
| pagination.limit <= maxGraphqlResultCount ->
|
|
pure Nothing
|
|
_ -> do
|
|
results <- liftIO $ SS.query_ connection countQuery
|
|
|
|
let numRows = case P.head results of
|
|
Just numRowsOnly -> SS.fromOnly numRowsOnly
|
|
Nothing -> 0
|
|
|
|
pure $
|
|
if numRows > maxGraphqlResultCount
|
|
then Just numRows
|
|
else Nothing
|
|
|
|
P.for_ tooManyReturnedRows $ \numRows -> do
|
|
P.throwIO $
|
|
userError $
|
|
P.fold
|
|
[ "The graphql API cannot return more than "
|
|
, show maxGraphqlResultCount
|
|
, " entries at a time. Your query would have returned "
|
|
, show numRows
|
|
, " rows. "
|
|
, "Consider setting the `limit` argument on your query: `{ "
|
|
, T.unpack tableName
|
|
, " (limit: 50) { ... } }`"
|
|
]
|
|
|
|
liftIO $
|
|
executeSqlQuery
|
|
connection
|
|
tableName
|
|
colEntries
|
|
filterElements
|
|
orderElements
|
|
paginationMb
|
|
|
|
rowsToList dbId tableName colEntries rows
|
|
|
|
getResolvers :: IO (HashMap.HashMap Text (Resolver IO))
|
|
getResolvers = do
|
|
let
|
|
getTableTuple :: TableEntryRaw -> IO (Text, Resolver IO)
|
|
getTableTuple table = do
|
|
outField <- getOutField table.name
|
|
pure
|
|
( doubleXEncodeGql table.name
|
|
, ValueResolver
|
|
outField
|
|
( -- Exceptions must be converted to ResolverExceptions
|
|
-- to be picked up by GQL query executor
|
|
catchAll
|
|
(getDbEntries table.name)
|
|
(throw . ResolverException)
|
|
)
|
|
)
|
|
|
|
getTableTuples :: IO [(Text, Resolver IO)]
|
|
getTableTuples =
|
|
P.for tables getTableTuple
|
|
|
|
getTableTuples <&> HashMap.fromList
|
|
|
|
-- -- TODO: Add support for retriving record by ID
|
|
-- getResolversPrimaryKey :: IO (HashMap.HashMap Text (Resolver IO))
|
|
-- getResolversPrimaryKey = do
|
|
-- let
|
|
-- getTableTuple table = do
|
|
-- outField <- getOutField $ table.name
|
|
-- pure
|
|
-- ( table.name) <> "_by_pk"
|
|
-- , ValueResolver
|
|
-- outField
|
|
-- (getDbEntries $ table.name)
|
|
-- )
|
|
|
|
-- getTableTuples :: IO [(Text, Resolver IO)]
|
|
-- getTableTuples =
|
|
-- sequence $ tables <&> getTableTuple
|
|
|
|
-- getTableTuples <&> HashMap.fromList
|
|
|
|
resolvers <- getResolvers
|
|
schemaResolver <- getSchemaResolver dbId connection accessMode tables
|
|
|
|
-- resolversPrimaryKey <- getResolversPrimaryKey
|
|
let
|
|
-- Resolve = ReaderT Context m Value
|
|
wrapResolve resolve = do
|
|
when (accessMode == WriteOnly) $ do
|
|
throw $
|
|
ResolverException $
|
|
userError "Cannot read field using writeonly access code"
|
|
resolve
|
|
|
|
protectResolver = \case
|
|
ValueResolver field resolve ->
|
|
ValueResolver field (wrapResolve resolve)
|
|
EventStreamResolver field resolve subscribe ->
|
|
EventStreamResolver field (wrapResolve resolve) subscribe
|
|
|
|
pure $
|
|
outObjectTypeToObjectType $
|
|
OutObjectType
|
|
{ name = "Query"
|
|
, descriptionMb = Just documentation
|
|
, interfaceTypes = []
|
|
, fields =
|
|
P.fold
|
|
[ schemaResolver
|
|
, typeNameResolver
|
|
, resolvers
|
|
-- , resolversPrimaryKey)
|
|
]
|
|
<&> protectResolver
|
|
}
|
|
|
|
|
|
-- | WARNING: Also change duplicate `sqlDataToAesonValue`
|
|
sqlDataToGQLValue :: Text -> SQLData -> Either Text Value
|
|
sqlDataToGQLValue datatype sqlData = case (datatype, sqlData) of
|
|
(_, SQLInteger int64) ->
|
|
if isInfixOf "BOOL" $ toUpper datatype
|
|
then pure $ case int64 of
|
|
0 -> Boolean False
|
|
_ -> Boolean True
|
|
else
|
|
if int64 >= fromIntegral (P.minBound :: P.Int32)
|
|
&& int64 <= fromIntegral (P.maxBound :: P.Int32)
|
|
then pure $ Int $ fromIntegral int64 -- Int32
|
|
else
|
|
Left $
|
|
"Integer "
|
|
<> show int64
|
|
<> " would overflow. "
|
|
<> "This happens because SQLite uses 64-bit ints, "
|
|
<> "but GraphQL uses 32-bit ints. "
|
|
<> "Use a Number (64-bit float) or Text column instead."
|
|
(_, SQLFloat double) -> pure $ Float double
|
|
(_, SQLText text) -> pure $ String text
|
|
(_, SQLBlob byteString) -> pure $ String $ show byteString
|
|
(_, SQLNull) -> pure Null
|
|
|
|
|
|
{-| Convert a GraphQL `Value` to a `SQLData`
|
|
TODO: ? -> SQLBlob $ string
|
|
-}
|
|
gqlValueToSQLData :: Value -> SQLData
|
|
gqlValueToSQLData = \case
|
|
Int int32 -> SQLInteger $ fromIntegral int32 -- Int64
|
|
Float double -> SQLFloat double
|
|
String text -> SQLText text
|
|
Null -> SQLNull
|
|
Boolean aBool ->
|
|
if aBool
|
|
then SQLInteger 1
|
|
else SQLInteger 0
|
|
Enum name -> SQLText name
|
|
List aList -> SQLText $ show aList
|
|
Object obj -> SQLText $ show obj
|
|
|
|
|
|
mutationTypeNameField :: Text -> (Text, Resolver IO)
|
|
mutationTypeNameField nameOfTable =
|
|
let
|
|
typeNameOutField =
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just $ "The type name of " <> nameOfTable
|
|
, fieldType = Out.NonNullScalarType string
|
|
, arguments = HashMap.empty
|
|
}
|
|
in
|
|
( "__typename"
|
|
, ValueResolver typeNameOutField $
|
|
pure $
|
|
String $
|
|
doubleXEncodeGql nameOfTable <> "_mutation_response"
|
|
)
|
|
|
|
|
|
getMutationResponse
|
|
:: Text
|
|
-> [ColumnEntry]
|
|
-> Out.Type IO
|
|
getMutationResponse tableName columnEntries =
|
|
Out.NamedObjectType $
|
|
outObjectTypeToObjectType $
|
|
OutObjectType
|
|
{ name = doubleXEncodeGql tableName <> "_mutation_response"
|
|
, descriptionMb =
|
|
Just $
|
|
tableName <> " mutation response description"
|
|
, interfaceTypes = []
|
|
, fields =
|
|
HashMap.fromList
|
|
[
|
|
( "affected_rows"
|
|
, let
|
|
field :: Out.Field m
|
|
field =
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just "nonNullInt description"
|
|
, fieldType = Out.NonNullScalarType int
|
|
, arguments = HashMap.empty
|
|
}
|
|
|
|
value :: ReaderT Out.Context IO Value
|
|
value = do
|
|
context <- ask
|
|
case context & Out.values of
|
|
Object obj ->
|
|
pure $
|
|
fromMaybe (Int 0) $
|
|
HashMap.lookup "affected_rows" obj
|
|
_ -> pure $ Int 0
|
|
in
|
|
ValueResolver field value
|
|
)
|
|
,
|
|
( "returning"
|
|
, let
|
|
field :: Out.Field IO
|
|
field =
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb =
|
|
Just
|
|
"Non null returning description"
|
|
, fieldType =
|
|
Out.NonNullListType $
|
|
Out.NamedObjectType $
|
|
Out.ObjectType
|
|
"returning"
|
|
(Just "short desc")
|
|
[]
|
|
( HashMap.fromList $
|
|
colNamesWithValResolver columnEntries
|
|
)
|
|
, arguments = HashMap.empty
|
|
}
|
|
|
|
value :: ReaderT Out.Context IO Value
|
|
value = do
|
|
context <- ask
|
|
case context & Out.values of
|
|
Object obj ->
|
|
pure $
|
|
fromMaybe (Object P.mempty) $
|
|
HashMap.lookup "returning" obj
|
|
_ -> pure $ Object P.mempty
|
|
in
|
|
ValueResolver field value
|
|
)
|
|
, mutationTypeNameField tableName
|
|
]
|
|
}
|
|
|
|
|
|
rowsToList :: (MonadIO m) => Text -> Text -> [ColumnEntry] -> [[SQLData]] -> m Value
|
|
rowsToList dbId tableName columnEntries updatedRows =
|
|
let
|
|
buildMetadataJson :: Text -> Text -> Text
|
|
buildMetadataJson colName rowid =
|
|
object ["url" .= colToFileUrl dbId tableName colName rowid]
|
|
& encodeToText
|
|
|
|
parseSqlData :: (ColumnEntry, SQLData) -> Either (Text, Text) (Text, Value)
|
|
parseSqlData (colEntry, colVal) =
|
|
if "BLOB" `T.isPrefixOf` colEntry.datatype
|
|
then
|
|
pure
|
|
( colEntry.column_name_gql
|
|
, case colVal of
|
|
SQLNull -> Null
|
|
SQLInteger id ->
|
|
String $
|
|
buildMetadataJson colEntry.column_name (show id)
|
|
SQLText id ->
|
|
String $
|
|
buildMetadataJson colEntry.column_name id
|
|
_ -> Null
|
|
)
|
|
else case sqlDataToGQLValue colEntry.datatype colVal of
|
|
Left err ->
|
|
Left
|
|
(colEntry.column_name_gql, err)
|
|
Right gqlData ->
|
|
Right
|
|
(colEntry.column_name_gql, gqlData)
|
|
in
|
|
updatedRows
|
|
<&> ( \row ->
|
|
-- => [(ColumnEntry, SQLData)]
|
|
P.zip columnEntries row
|
|
-- => [Either (Text, Text) (Text, Value)]
|
|
<&> parseSqlData
|
|
-- => Either [(Text, Text)] (Text, Value)
|
|
& collectErrorList
|
|
-- => Either [(Text, Text)] (HashMap Text Value)
|
|
<&> HashMap.fromList
|
|
-- => Either [(Text, Text)] Value
|
|
<&> Object
|
|
)
|
|
-- => Either [[(Text, Text)]] [Value]
|
|
& collectErrorList
|
|
& \case
|
|
Right values -> pure $ List values
|
|
Left errors ->
|
|
let
|
|
errorLines =
|
|
P.join errors
|
|
<&> \(column, err) -> "On column " <> show column <> ": " <> err
|
|
in
|
|
P.throwIO $
|
|
userError $
|
|
T.unpack $
|
|
"Multiple errors occurred:\n" <> P.unlines errorLines
|
|
|
|
|
|
executeSqlMutation
|
|
:: Connection
|
|
-> Text
|
|
-> HashMap.HashMap Text Value
|
|
-> [ColumnEntry]
|
|
-> [(Text, Value)]
|
|
-> IO (Int, [[SQLData]])
|
|
executeSqlMutation connection tableName args columnEntries filterElements = do
|
|
let
|
|
colNamesToUpdateRaw :: [Text]
|
|
colNamesToUpdateRaw =
|
|
case HashMap.lookup "set" args of
|
|
Just (Object dataObj) -> HashMap.keys dataObj
|
|
_ -> []
|
|
|
|
colNamesToUpdate :: [Text]
|
|
colNamesToUpdate =
|
|
columnEntries
|
|
<&> column_name
|
|
<&> ( \columnName ->
|
|
if doubleXEncodeGql columnName `P.elem` colNamesToUpdateRaw
|
|
then Just columnName
|
|
else Nothing
|
|
)
|
|
& P.catMaybes
|
|
|
|
columnNamesText :: Text
|
|
columnNamesText =
|
|
columnEntries
|
|
<&> column_name
|
|
<&> quoteKeyword
|
|
& intercalate ", "
|
|
|
|
setText :: Text
|
|
setText =
|
|
colNamesToUpdate
|
|
<&> (\columnName -> quoteKeyword columnName <> " = ?")
|
|
& intercalate ", "
|
|
|
|
valuesToSet :: [SQLData]
|
|
valuesToSet =
|
|
case HashMap.lookup "set" args of
|
|
Just (Object dataObj) ->
|
|
columnEntries
|
|
<&> column_name
|
|
<&> ( \columnName ->
|
|
HashMap.lookup
|
|
(doubleXEncodeGql columnName)
|
|
dataObj
|
|
)
|
|
& P.catMaybes
|
|
<&> gqlValueToSQLData
|
|
_ -> []
|
|
|
|
updatedRows :: [[SQLData]] <-
|
|
if setText == ""
|
|
then pure []
|
|
else
|
|
let
|
|
sqlQuery =
|
|
Query $
|
|
"UPDATE "
|
|
<> quoteKeyword tableName
|
|
<> "\n"
|
|
<> "SET "
|
|
<> setText
|
|
<> "\n"
|
|
<> getWhereClause filterElements
|
|
<> "\n"
|
|
<> "RETURNING "
|
|
<> columnNamesText
|
|
|
|
colTypesToUpdate :: [Text]
|
|
colTypesToUpdate =
|
|
columnEntries
|
|
<&> ( \colEntry ->
|
|
if doubleXEncodeGql colEntry.column_name
|
|
`P.elem` colNamesToUpdateRaw
|
|
then Just colEntry.datatype
|
|
else Nothing
|
|
)
|
|
& P.catMaybes
|
|
|
|
valuesToSetNorm =
|
|
P.zip valuesToSet colTypesToUpdate
|
|
<&> \(val, datatype) ->
|
|
if (val == SQLText "{}")
|
|
P.&& ("BLOB" `T.isPrefixOf` T.toUpper datatype)
|
|
then SQLBlob ""
|
|
else val
|
|
in
|
|
catchAll
|
|
( liftIO $ do
|
|
setCaseInsensitive connection filterElements
|
|
query connection sqlQuery valuesToSetNorm
|
|
)
|
|
(throw . ResolverException)
|
|
|
|
liftIO $
|
|
changes connection
|
|
& P.fmap (,updatedRows)
|
|
|
|
|
|
mutationType
|
|
:: Connection
|
|
-> Integer
|
|
-> Text
|
|
-> [TableEntryRaw]
|
|
-> IO (Maybe (Out.ObjectType IO))
|
|
mutationType connection maxRowsPerTable dbId tables = do
|
|
let
|
|
documentation =
|
|
"Available queries for database \"" <> dbId <> "\""
|
|
|
|
getTableFilterType :: Text -> [ColumnEntry] -> InputObjectType
|
|
getTableFilterType tableName columnEntries = do
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_filter")
|
|
( Just
|
|
"Filter objects for the specified columns"
|
|
)
|
|
(HashMap.fromList (colNamesWithFilterField tableName columnEntries))
|
|
|
|
getOutField :: Text -> IO (Out.Field IO)
|
|
getOutField tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
let
|
|
colNamesWithField :: [(Text, InputField)]
|
|
colNamesWithField =
|
|
columnEntries <&> \colEntry ->
|
|
let
|
|
inputField =
|
|
InputField
|
|
(Just colEntry.column_name_gql)
|
|
( In.NamedScalarType $
|
|
typeNameToScalarType colEntry.datatype_gql
|
|
)
|
|
Nothing -- Default value
|
|
in
|
|
( colEntry.column_name_gql
|
|
, inputField
|
|
)
|
|
|
|
let
|
|
objectsType =
|
|
inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb =
|
|
Just
|
|
"Objects to be inserted into the database"
|
|
, argType =
|
|
In.ListType $
|
|
NamedInputObjectType $
|
|
InputObjectType
|
|
( doubleXEncodeGql tableName
|
|
<> "_insert_input"
|
|
)
|
|
( Just
|
|
"Object to be inserted into the database"
|
|
)
|
|
(HashMap.fromList colNamesWithField)
|
|
, valueMb = Nothing
|
|
}
|
|
|
|
onConflictDescription =
|
|
"Specifies how to handle brtoken unique constraints" :: Text
|
|
|
|
columnEnumVariants =
|
|
columnEntries
|
|
<&> \entry ->
|
|
(entry.column_name_gql, EnumValue Nothing)
|
|
|
|
columnEnumType =
|
|
EnumType
|
|
(doubleXEncodeGql tableName <> "_column")
|
|
(Just "This enum contains a variant for each colum in the table")
|
|
(HashMap.fromList columnEnumVariants)
|
|
|
|
onConflictType =
|
|
inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just onConflictDescription
|
|
, argType =
|
|
In.ListType
|
|
$ In.NonNullInputObjectType
|
|
$ InputObjectType
|
|
( doubleXEncodeGql tableName
|
|
<> "_upsert_on_conflict"
|
|
)
|
|
(Just onConflictDescription)
|
|
$ HashMap.fromList
|
|
[
|
|
( "constraint"
|
|
, InputField
|
|
(Just "columns to handle conflicts of")
|
|
( In.NonNullListType $
|
|
In.NonNullEnumType columnEnumType
|
|
)
|
|
Nothing
|
|
)
|
|
,
|
|
( "update_columns"
|
|
, InputField
|
|
(Just "columns to override on conflict")
|
|
( In.NonNullListType $
|
|
In.NonNullEnumType columnEnumType
|
|
)
|
|
Nothing
|
|
)
|
|
,
|
|
( "where"
|
|
, InputField
|
|
(Just "filter specifying which conflicting columns to update")
|
|
( In.NamedInputObjectType $
|
|
getTableFilterType tableName columnEntries
|
|
)
|
|
Nothing
|
|
)
|
|
]
|
|
, valueMb = Nothing
|
|
}
|
|
|
|
pure $
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just "description"
|
|
, fieldType = getMutationResponse tableName columnEntries
|
|
, arguments =
|
|
HashMap.fromList
|
|
[ ("objects", objectsType)
|
|
, ("on_conflict", onConflictType)
|
|
]
|
|
}
|
|
|
|
getColValue :: HashMap.HashMap Text Value -> Text -> Value
|
|
getColValue rowObj columnName =
|
|
HashMap.findWithDefault Null (doubleXEncodeGql columnName) rowObj
|
|
|
|
executeDbInserts :: Text -> ReaderT Out.Context IO Value
|
|
executeDbInserts tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
context <- ask
|
|
|
|
let
|
|
columnNames :: [Text]
|
|
columnNames =
|
|
columnEntries <&> column_name
|
|
|
|
columnNamesText :: Text
|
|
columnNamesText =
|
|
columnNames
|
|
<&> quoteKeyword
|
|
& intercalate ", "
|
|
|
|
insertInDb :: Arguments -> ReaderT Out.Context IO (Int, [[SQLData]])
|
|
insertInDb (Arguments argMap) = do
|
|
let
|
|
-- Yields for example:
|
|
-- [ { name: "John", email: "john@example.com" }
|
|
-- , { name: "Eve", email: "eve@example.com" }
|
|
-- ]
|
|
entries =
|
|
HashMap.findWithDefault
|
|
(List [])
|
|
"objects"
|
|
argMap
|
|
|
|
-- All colums that are contained in the entries
|
|
containedColumns :: [Text]
|
|
containedColumns =
|
|
case entries of
|
|
List values ->
|
|
( values
|
|
<&> \case
|
|
Object rowObj ->
|
|
HashMap.keys rowObj
|
|
_ -> []
|
|
)
|
|
& P.concat
|
|
& nub
|
|
<&> doubleXDecode
|
|
_ -> []
|
|
|
|
boundVariableNames :: [Text]
|
|
boundVariableNames =
|
|
containedColumns
|
|
<&> (\name -> ":" <> doubleXEncodeGql name)
|
|
|
|
onConflictArg =
|
|
case HashMap.lookup "on_conflict" argMap of
|
|
Just (List values) -> values
|
|
_ -> []
|
|
|
|
onConflictClauses <- P.for onConflictArg $ \case
|
|
Object fields -> do
|
|
let
|
|
getColumnList fieldName = do
|
|
case HashMap.lookup fieldName fields of
|
|
Just (List elements) -> do
|
|
element <- elements
|
|
case element of
|
|
Enum columnName -> pure columnName
|
|
_ -> []
|
|
_ -> []
|
|
|
|
constraint = getColumnList "constraint"
|
|
update = getColumnList "update_columns"
|
|
|
|
updateClauses <- P.for update $ \column -> do
|
|
when (column `notElem` containedColumns) $ do
|
|
P.throwIO $
|
|
userError $
|
|
"Column "
|
|
<> T.unpack column
|
|
<> " cannot be set on conflicts without being explicitly provided"
|
|
|
|
pure $
|
|
quoteKeyword column
|
|
<> " = :"
|
|
<> doubleXEncodeGql column
|
|
|
|
let
|
|
filterElements = case HashMap.lookup "where" fields of
|
|
Just (Object filterObj) -> HashMap.toList filterObj
|
|
_ -> []
|
|
|
|
pure $
|
|
"ON CONFLICT ("
|
|
<> ( constraint
|
|
<&> quoteKeyword
|
|
& intercalate "<>"
|
|
)
|
|
<> ")\n DO UPDATE SET \n"
|
|
<> intercalate ",\n" updateClauses
|
|
<> "\n"
|
|
<> getWhereClause filterElements
|
|
_ -> pure ""
|
|
|
|
let
|
|
columnList =
|
|
if P.null containedColumns
|
|
then ""
|
|
else
|
|
" ("
|
|
<> ( containedColumns
|
|
<&> quoteKeyword
|
|
& intercalate ", "
|
|
)
|
|
<> ")"
|
|
insertedValues =
|
|
if P.null boundVariableNames
|
|
then "DEFAULT VALUES"
|
|
else
|
|
"VALUES ("
|
|
<> intercalate ", " boundVariableNames
|
|
<> ")"
|
|
sqlQuery =
|
|
Query $
|
|
"INSERT INTO "
|
|
<> quoteKeyword tableName
|
|
<> columnList
|
|
<> insertedValues
|
|
<> "\n"
|
|
<> P.unlines onConflictClauses
|
|
<> "RETURNING "
|
|
<>
|
|
-- TODO: Only return the actually requested values
|
|
columnNamesText
|
|
|
|
sqlDataRows :: [[SQLData]]
|
|
sqlDataRows =
|
|
case entries of
|
|
List values ->
|
|
values <&> \case
|
|
Object rowObj ->
|
|
containedColumns
|
|
<&> getColValue rowObj
|
|
<&> gqlValueToSQLData
|
|
_ -> []
|
|
_ -> []
|
|
|
|
-- Exception from SQLite must be converted into
|
|
-- ResolverExceptions to be picked up by GQL query executor
|
|
returnedRows <-
|
|
catchAll
|
|
( liftIO $ P.forM sqlDataRows $ \sqlDataRow -> do
|
|
numRowsRes :: [[Integer]] <-
|
|
query_
|
|
connection
|
|
$ Query
|
|
$ "SELECT COUNT() FROM "
|
|
<> quoteKeyword tableName
|
|
|
|
case numRowsRes of
|
|
[[numRows]] -> do
|
|
when (numRows >= maxRowsPerTable) $
|
|
P.throwIO $
|
|
userError $
|
|
"Please upgrade to a Pro account \
|
|
\to insert more than "
|
|
<> show maxRowsPerTable
|
|
<> " rows into a table"
|
|
_ -> pure ()
|
|
|
|
SS.queryNamed connection sqlQuery $
|
|
P.zipWith (SS.:=) boundVariableNames sqlDataRow
|
|
)
|
|
(throw . ResolverException)
|
|
|
|
-- FIXME:
|
|
-- This should probably be used, but sqlite-simple
|
|
-- doesn't use only one query to execute the insert
|
|
-- https://github.com/nurpax/sqlite-simple/issues/82
|
|
-- liftIO $ changes connection
|
|
pure (P.length sqlDataRows, returnedRows & P.concat)
|
|
|
|
(numOfChanges, returnedRows) <- insertInDb context.arguments
|
|
returning <- rowsToList dbId tableName columnEntries returnedRows
|
|
|
|
pure $
|
|
Object $
|
|
HashMap.fromList
|
|
[ ("affected_rows", Int $ fromIntegral numOfChanges)
|
|
, ("returning", returning)
|
|
]
|
|
|
|
-- Execute SQL query to update selected entries
|
|
executeDbUpdates :: Text -> ReaderT Out.Context IO Value
|
|
executeDbUpdates tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
context <- ask
|
|
|
|
let Arguments args = context.arguments
|
|
|
|
(numOfChanges, updatedRows) <- case HashMap.lookup "filter" args of
|
|
Just (Object filterObj) -> case HashMap.toList filterObj of
|
|
[] -> P.throwIO $ userError "Error: Filter must not be empty"
|
|
filterElements ->
|
|
liftIO $
|
|
executeSqlMutation
|
|
connection
|
|
tableName
|
|
args
|
|
columnEntries
|
|
filterElements
|
|
_ -> pure (0, [])
|
|
|
|
returning <- rowsToList dbId tableName columnEntries updatedRows
|
|
|
|
pure $
|
|
Object $
|
|
HashMap.fromList
|
|
[ ("affected_rows", Int $ fromIntegral (numOfChanges :: Int))
|
|
, ("returning", returning)
|
|
]
|
|
|
|
-- Execute SQL query to delete selected entries
|
|
executeDbDeletions :: Text -> ReaderT Out.Context IO Value
|
|
executeDbDeletions tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
context <- ask
|
|
|
|
let
|
|
columnNamesText :: Text
|
|
columnNamesText =
|
|
columnEntries
|
|
<&> column_name
|
|
<&> quoteKeyword
|
|
& intercalate ", "
|
|
|
|
deleteEntry columnName value = do
|
|
let sqlQuery =
|
|
Query $
|
|
"DELETE FROM "
|
|
<> quoteKeyword tableName
|
|
<> " \
|
|
\WHERE "
|
|
<> quoteKeyword columnName
|
|
<> " = ?\n"
|
|
<> "RETURNING "
|
|
<> columnNamesText
|
|
deletedRows :: [[SQLData]] <-
|
|
catchAll
|
|
(liftIO $ query connection sqlQuery [value])
|
|
(throw . ResolverException)
|
|
numChanges <- liftIO $ changes connection
|
|
|
|
pure (numChanges, deletedRows)
|
|
|
|
(numOfChanges, deletedRows) <- case context.arguments of
|
|
Arguments args -> case HashMap.lookup "filter" args of
|
|
Just colToFilter -> case colToFilter of
|
|
Object filterObj -> case HashMap.toList filterObj of
|
|
[(columnName, Object operatorAndValue)] -> do
|
|
case HashMap.toList operatorAndValue of
|
|
[("eq", String value)] ->
|
|
deleteEntry columnName value
|
|
[("eq", Int value)] ->
|
|
deleteEntry columnName $ show value
|
|
_ -> pure (0, [])
|
|
_ -> pure (0, [])
|
|
_ -> pure (0, [])
|
|
Nothing -> pure (0, [])
|
|
|
|
returning <- rowsToList dbId tableName columnEntries deletedRows
|
|
|
|
pure $
|
|
Object $
|
|
HashMap.fromList
|
|
[ ("affected_rows", Int $ fromIntegral numOfChanges)
|
|
, ("returning", returning)
|
|
]
|
|
|
|
getOutFieldUpdate :: Text -> IO (Out.Field IO)
|
|
getOutFieldUpdate tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
let
|
|
colNamesWithField :: [(Text, InputField)]
|
|
colNamesWithField =
|
|
columnEntries <&> \colEntry ->
|
|
let
|
|
inputField =
|
|
InputField
|
|
(Just colEntry.column_name_gql)
|
|
( In.NamedScalarType $
|
|
typeNameToScalarType colEntry.datatype_gql
|
|
)
|
|
Nothing -- Default value
|
|
in
|
|
( colEntry.column_name_gql
|
|
, inputField
|
|
)
|
|
|
|
pure $
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just $ "Provides entries from " <> tableName
|
|
, fieldType = getMutationResponse tableName columnEntries
|
|
, arguments =
|
|
HashMap.fromList
|
|
[
|
|
( "filter"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just "Filter objects"
|
|
, argType =
|
|
NamedInputObjectType $
|
|
getTableFilterType tableName columnEntries
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
,
|
|
( "set"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just "Map with new values"
|
|
, argType =
|
|
NamedInputObjectType $
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_set_input")
|
|
(Just "New values for the specified columns")
|
|
(HashMap.fromList colNamesWithField)
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
]
|
|
}
|
|
|
|
getOutFieldDeletion :: Text -> IO (Out.Field IO)
|
|
getOutFieldDeletion tableName = do
|
|
columnEntries <- liftIO $ getColumns dbId connection tableName
|
|
|
|
pure $
|
|
outFieldToField $
|
|
OutField
|
|
{ descriptionMb = Just $ "Provides entries from " <> tableName
|
|
, fieldType = getMutationResponse tableName columnEntries
|
|
, arguments =
|
|
HashMap.fromList
|
|
[
|
|
( "filter"
|
|
, inArgumentToArgument $
|
|
InArgument
|
|
{ argDescMb = Just "Filter objects"
|
|
, argType =
|
|
NamedInputObjectType $
|
|
InputObjectType
|
|
(doubleXEncodeGql tableName <> "_filter")
|
|
( Just
|
|
"Filter objects for the specified columns"
|
|
)
|
|
(HashMap.fromList (colNamesWithFilterField tableName columnEntries))
|
|
, valueMb = Nothing
|
|
}
|
|
)
|
|
]
|
|
}
|
|
-- -- TODO: Use for retrieving record by primary key
|
|
-- , arguments = HashMap.fromList $ columnEntries
|
|
-- <&> (\colEntry ->
|
|
-- ( colEntry & column_name_gql :: Text
|
|
-- , inArgumentToArgument $ InArgument
|
|
-- { argDescMb = Just "Retrieve object by primary key"
|
|
-- , argType = In.NamedScalarType $
|
|
-- typeNameToScalarType $ colEntry & datatype
|
|
-- , valueMb = Nothing
|
|
-- }
|
|
-- )
|
|
-- )
|
|
|
|
getMutationResolvers :: IO (HashMap.HashMap Text (Resolver IO))
|
|
getMutationResolvers = do
|
|
let
|
|
getInsertTableTuple :: TableEntryRaw -> IO (Text, Resolver IO)
|
|
getInsertTableTuple table = do
|
|
outFieldInsertion <- getOutField table.name
|
|
pure
|
|
( "insert_" <> doubleXEncodeGql table.name
|
|
, ValueResolver
|
|
outFieldInsertion
|
|
(executeDbInserts table.name)
|
|
)
|
|
|
|
getUpdateTableTuple :: TableEntryRaw -> IO (Text, Resolver IO)
|
|
getUpdateTableTuple table = do
|
|
outFieldUpdate <- getOutFieldUpdate table.name
|
|
pure
|
|
( "update_" <> doubleXEncodeGql table.name
|
|
, ValueResolver
|
|
outFieldUpdate
|
|
(executeDbUpdates table.name)
|
|
)
|
|
|
|
getDeleteTableTuple :: TableEntryRaw -> IO (Text, Resolver IO)
|
|
getDeleteTableTuple table = do
|
|
outFieldDeletion <- getOutFieldDeletion table.name
|
|
pure
|
|
( "delete_" <> doubleXEncodeGql table.name
|
|
, ValueResolver
|
|
outFieldDeletion
|
|
(executeDbDeletions table.name)
|
|
)
|
|
|
|
getTableTuples :: IO [(Text, Resolver IO)]
|
|
getTableTuples =
|
|
sequence $
|
|
(tables <&> getInsertTableTuple)
|
|
<> (tables <&> getUpdateTableTuple)
|
|
<> (tables <&> getDeleteTableTuple)
|
|
|
|
getTableTuples <&> HashMap.fromList
|
|
|
|
Just
|
|
. Out.ObjectType
|
|
"Mutation"
|
|
(Just documentation)
|
|
[]
|
|
<$> getMutationResolvers
|
|
|
|
|
|
-- | Automatically generated schema derived from the SQLite database
|
|
getDerivedSchema
|
|
:: SchemaConf
|
|
-> Connection
|
|
-> Text
|
|
-> [TableEntryRaw]
|
|
-> IO (Schema IO)
|
|
getDerivedSchema schemaConf connection dbId tables = do
|
|
sqlitePragmas <- getSQLitePragmas schemaConf.pragmaConf
|
|
P.forM_ sqlitePragmas (execute_ connection)
|
|
|
|
queries <- queryType connection schemaConf.accessMode dbId tables
|
|
mutations <-
|
|
mutationType
|
|
connection
|
|
schemaConf.maxRowsPerTable
|
|
dbId
|
|
tables
|
|
|
|
pure $
|
|
schema
|
|
queries
|
|
( case schemaConf.accessMode of
|
|
ReadOnly -> Nothing
|
|
WriteOnly -> mutations
|
|
ReadAndWrite -> mutations
|
|
)
|
|
Nothing -- subscriptions
|
|
mempty
|