mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
29158900d8
Source typename customization (hasura/graphql-engine@aac64f2c81) 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
175 lines
6.3 KiB
Haskell
175 lines
6.3 KiB
Haskell
-- | 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
|
|
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 =>
|
|
Name ->
|
|
Maybe Description ->
|
|
Parser 'Both n (InputValue Variable)
|
|
unsafeRawScalar name description =
|
|
Parser
|
|
{ pType = NonNullable $ TNamed $ mkDefinition name description TIScalar,
|
|
pParser = pure
|
|
}
|
|
|
|
-- | Creates a parser that transforms its input into a JSON value. 'valueToJSON'
|
|
-- does properly unpack variables.
|
|
jsonScalar :: MonadParse m => Name -> Maybe Description -> Parser 'Both m A.Value
|
|
jsonScalar name description =
|
|
Parser
|
|
{ pType = schemaType,
|
|
pParser = valueToJSON $ toGraphQLType schemaType
|
|
}
|
|
where
|
|
schemaType = NonNullable $ TNamed $ mkDefinition 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 = NonNullable $ TNamed $ mkDefinition name description TIScalar
|
|
|
|
convertWith ::
|
|
MonadParse m =>
|
|
(a -> A.Parser b) ->
|
|
(a -> m b)
|
|
convertWith f x = runAesonParser f x `onLeft` (parseErrorWith ParseFailed . qeError)
|