graphql-engine/server/src-lib/Hasura/GraphQL/Parser/Internal/Scalars.hs

175 lines
6.3 KiB
Haskell
Raw Normal View History

-- | This module defines all backend-agnostic scalars we use throughout the
-- schema. This includes GraphQL scalars, and several other custom ones.
module Hasura.GraphQL.Parser.Internal.Scalars
( -- built-in types
boolean,
int,
float,
string,
identifier,
-- custom extensions
uuid,
json,
jsonb,
nonNegativeInt,
bigInt,
-- internal
unsafeRawScalar,
jsonScalar,
)
where
import Data.Aeson qualified as A
import Data.Aeson.Types qualified as A
import Data.Int (Int32, Int64)
import Data.Text.Read (decimal)
import Data.UUID qualified as UUID
import Hasura.Backends.Postgres.SQL.Value
import Hasura.Base.Error
import Hasura.GraphQL.Parser.Class.Parse
import Hasura.GraphQL.Parser.Internal.Convert
import Hasura.GraphQL.Parser.Internal.TypeChecking
import Hasura.GraphQL.Parser.Internal.Types
import Hasura.GraphQL.Parser.Schema
import Hasura.Prelude
import Hasura.RQL.Types.CustomTypes
import Language.GraphQL.Draft.Syntax hiding (Definition)
--------------------------------------------------------------------------------
-- Built-in scalars
boolean :: MonadParse m => Parser 'Both m Bool
boolean = mkScalar boolScalar Nothing \case
GraphQLValue (VBoolean b) -> pure b
JSONValue (A.Bool b) -> pure b
v -> typeMismatch boolScalar "a boolean" v
int :: MonadParse m => Parser 'Both m Int32
int = mkScalar intScalar Nothing \case
GraphQLValue (VInt i) -> convertWith scientificToInteger $ fromInteger i
JSONValue (A.Number n) -> convertWith scientificToInteger n
v -> typeMismatch intScalar "a 32-bit integer" v
float :: MonadParse m => Parser 'Both m Double
float = mkScalar floatScalar Nothing \case
GraphQLValue (VFloat f) -> convertWith scientificToFloat f
GraphQLValue (VInt i) -> convertWith scientificToFloat $ fromInteger i
JSONValue (A.Number n) -> convertWith scientificToFloat n
v -> typeMismatch floatScalar "a float" v
string :: MonadParse m => Parser 'Both m Text
string = mkScalar stringScalar Nothing \case
GraphQLValue (VString s) -> pure s
JSONValue (A.String s) -> pure s
v -> typeMismatch stringScalar "a string" v
-- | As an input type, any string or integer input value should be coerced to ID as Text
-- https://spec.graphql.org/June2018/#sec-ID
identifier :: MonadParse m => Parser 'Both m Text
identifier = mkScalar idScalar Nothing \case
GraphQLValue (VString s) -> pure s
GraphQLValue (VInt i) -> pure $ tshow i
JSONValue (A.String s) -> pure s
JSONValue (A.Number n) -> parseScientific n
v -> typeMismatch idScalar "a String or a 32-bit integer" v
where
parseScientific = convertWith $ fmap (tshow @Int) . scientificToInteger
--------------------------------------------------------------------------------
-- Custom scalars
uuid :: MonadParse m => Parser 'Both m UUID.UUID
uuid = mkScalar name Nothing \case
GraphQLValue (VString s) -> convertWith A.parseJSON $ A.String s
JSONValue v -> convertWith A.parseJSON v
v -> typeMismatch name "a UUID" v
where
name = $$(litName "uuid")
json, jsonb :: MonadParse m => Parser 'Both m A.Value
Refactor type name customization Source typename customization (hasura/graphql-engine@aac64f2c81faa6a3aef4d0cf5fae97289ac4383e) introduced a mechanism to change certain names in the GraphQL schema that is exposed. In particular it allows last-minute modification of: 1. the names of some types, and 2. the names of some root fields. The above two items are assigned distinct customization algorithms, and at times both algorithms are in scope. So a need to distinguish them is needed. In the original design, this was addressed by introducing a newtype wrapper `Typename` around GraphQL `Name`s, dedicated to the names of types. However, in the majority of the codebase, type names are also represented by `Name`. For this reason, it was unavoidable to allow for easy conversion. This was supported by a `HasName Typename` instance, as well as by publishing the constructors of `Typename`. This means that the type safety that newtypes can add is lost. In particular, it is now very easy to confuse type name customization with root field name customization. This refactors the above design by instead introducing newtypes around the customization operations: ```haskell newtype MkTypename = MkTypename {runMkTypename :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) newtype MkRootFieldName = MkRootFieldName {runMkRootFieldName :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) ``` The `Monoid` instance allows easy composition of customization operations, piggybacking off of the type of `Endo`maps. This design allows safe co-existence of the two customization algorithms, while avoiding the syntactic overhead of packing and unpacking newtypes. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2989 GitOrigin-RevId: da3a353a9b003ee40c8d0a1e02872e99d2edd3ca
2021-11-30 12:51:46 +03:00
json = jsonScalar $$(litName "json") Nothing
jsonb = jsonScalar $$(litName "jsonb") Nothing
-- | Additional validation on integers. We do keep the same type name in the schema for backwards
-- compatibility.
-- TODO: when we can do a breaking change, we can rename the type to "NonNegativeInt".
nonNegativeInt :: MonadParse m => Parser 'Both m Int32
nonNegativeInt = mkScalar intScalar Nothing \case
GraphQLValue (VInt i) | i >= 0 -> convertWith scientificToInteger $ fromInteger i
JSONValue (A.Number n) | n >= 0 -> convertWith scientificToInteger n
v -> typeMismatch intScalar "a non-negative 32-bit integer" v
-- | GraphQL ints are 32-bit integers; but in some places we want to accept bigger ints. To do so,
-- we declare a cusom scalar that can represent 64-bit ints, which accepts both int literals and
-- string literals. We do keep the same type name in the schema for backwards compatibility.
-- TODO: when we can do a breaking change, we can rename the type to "BigInt".
bigInt :: MonadParse m => Parser 'Both m Int64
bigInt = mkScalar intScalar Nothing \case
GraphQLValue (VInt i) -> convertWith scientificToInteger $ fromInteger i
JSONValue (A.Number n) -> convertWith scientificToInteger n
GraphQLValue (VString s)
| Right (i, "") <- decimal s ->
pure i
JSONValue (A.String s)
| Right (i, "") <- decimal s ->
pure i
v -> typeMismatch intScalar "a 32-bit integer, or a 64-bit integer represented as a string" v
--------------------------------------------------------------------------------
-- Internal tools
-- | Explicitly define any desired scalar type.
--
-- This was considered unsafe because, unlike all other scalar definitions, it
-- doesn't enforce that we properly peel variables, and let the caller decide
-- how they want to deal with potential variables. This allows the caller to
-- bypass type-checking and (deprecated) non-reusability semantics (see comment
-- about re-usability in TypeChecking).
--
-- In practice, this function isn't very dangerous, and preferable to an
-- explicit use of the Parser constructor.
unsafeRawScalar ::
MonadParse n =>
Refactor type name customization Source typename customization (hasura/graphql-engine@aac64f2c81faa6a3aef4d0cf5fae97289ac4383e) introduced a mechanism to change certain names in the GraphQL schema that is exposed. In particular it allows last-minute modification of: 1. the names of some types, and 2. the names of some root fields. The above two items are assigned distinct customization algorithms, and at times both algorithms are in scope. So a need to distinguish them is needed. In the original design, this was addressed by introducing a newtype wrapper `Typename` around GraphQL `Name`s, dedicated to the names of types. However, in the majority of the codebase, type names are also represented by `Name`. For this reason, it was unavoidable to allow for easy conversion. This was supported by a `HasName Typename` instance, as well as by publishing the constructors of `Typename`. This means that the type safety that newtypes can add is lost. In particular, it is now very easy to confuse type name customization with root field name customization. This refactors the above design by instead introducing newtypes around the customization operations: ```haskell newtype MkTypename = MkTypename {runMkTypename :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) newtype MkRootFieldName = MkRootFieldName {runMkRootFieldName :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) ``` The `Monoid` instance allows easy composition of customization operations, piggybacking off of the type of `Endo`maps. This design allows safe co-existence of the two customization algorithms, while avoiding the syntactic overhead of packing and unpacking newtypes. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2989 GitOrigin-RevId: da3a353a9b003ee40c8d0a1e02872e99d2edd3ca
2021-11-30 12:51:46 +03:00
Name ->
Maybe Description ->
Parser 'Both n (InputValue Variable)
unsafeRawScalar name description =
Parser
{ pType = TNamed NonNullable $ Definition name description TIScalar,
pParser = pure
}
-- | Creates a parser that transforms its input into a JSON value. 'valueToJSON'
-- does properly unpack variables.
Refactor type name customization Source typename customization (hasura/graphql-engine@aac64f2c81faa6a3aef4d0cf5fae97289ac4383e) introduced a mechanism to change certain names in the GraphQL schema that is exposed. In particular it allows last-minute modification of: 1. the names of some types, and 2. the names of some root fields. The above two items are assigned distinct customization algorithms, and at times both algorithms are in scope. So a need to distinguish them is needed. In the original design, this was addressed by introducing a newtype wrapper `Typename` around GraphQL `Name`s, dedicated to the names of types. However, in the majority of the codebase, type names are also represented by `Name`. For this reason, it was unavoidable to allow for easy conversion. This was supported by a `HasName Typename` instance, as well as by publishing the constructors of `Typename`. This means that the type safety that newtypes can add is lost. In particular, it is now very easy to confuse type name customization with root field name customization. This refactors the above design by instead introducing newtypes around the customization operations: ```haskell newtype MkTypename = MkTypename {runMkTypename :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) newtype MkRootFieldName = MkRootFieldName {runMkRootFieldName :: Name -> Name} deriving (Semigroup, Monoid) via (Endo Name) ``` The `Monoid` instance allows easy composition of customization operations, piggybacking off of the type of `Endo`maps. This design allows safe co-existence of the two customization algorithms, while avoiding the syntactic overhead of packing and unpacking newtypes. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2989 GitOrigin-RevId: da3a353a9b003ee40c8d0a1e02872e99d2edd3ca
2021-11-30 12:51:46 +03:00
jsonScalar :: MonadParse m => Name -> Maybe Description -> Parser 'Both m A.Value
jsonScalar name description =
Parser
{ pType = schemaType,
pParser = valueToJSON $ toGraphQLType schemaType
}
where
schemaType = TNamed NonNullable $ Definition name description TIScalar
--------------------------------------------------------------------------------
-- Local helpers
mkScalar ::
MonadParse m =>
Name ->
Maybe Description ->
(InputValue Variable -> m a) ->
Parser 'Both m a
mkScalar name description parser =
Parser
{ pType = schemaType,
pParser = peelVariable (toGraphQLType schemaType) >=> parser
}
where
schemaType = TNamed NonNullable $ Definition name description TIScalar
convertWith ::
MonadParse m =>
(a -> A.Parser b) ->
(a -> m b)
convertWith f x = runAesonParser f x `onLeft` (parseErrorWith ParseFailed . qeError)