mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
server: support for Apollo federation
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4584 Co-authored-by: Auke Booij <164426+abooij@users.noreply.github.com> GitOrigin-RevId: 0f60c263efb5fbaa25620dd8159e8cfda25a61b2
This commit is contained in:
parent
73d8d35df3
commit
95adde4ce2
@ -36,6 +36,7 @@ post-webhook
|
||||
get-webhook
|
||||
insecure-webhook
|
||||
insecure-webhook-with-admin-secret
|
||||
apollo-federation
|
||||
allowlist-queries
|
||||
jwk-url
|
||||
horizontal-scaling
|
||||
|
@ -1024,6 +1024,22 @@ insecure-webhook-with-admin-secret)
|
||||
kill $WH_PID
|
||||
;;
|
||||
|
||||
apollo-federation)
|
||||
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH APOLLO FEDERATION ########>\n"
|
||||
|
||||
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM"
|
||||
export HASURA_GRAPHQL_EXPERIMENTAL_FEATURES="apollo_federation"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest -n 1 --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" test_apollo_federation.py
|
||||
|
||||
unset HASURA_GRAPHQL_EXPERIMENTAL_FEATURES
|
||||
unset HASURA_GRAPHQL_ADMIN_SECRET
|
||||
|
||||
kill_hge_servers
|
||||
;;
|
||||
|
||||
allowlist-queries)
|
||||
# allowlist queries test
|
||||
# unset HASURA_GRAPHQL_AUTH_HOOK
|
||||
|
42
CHANGELOG.md
42
CHANGELOG.md
@ -2,6 +2,48 @@
|
||||
|
||||
## Next release
|
||||
|
||||
### Introducing Apollo Federation v1 support (experimental)
|
||||
|
||||
HGE can now be used as a subgraph in an Apollo federated graphql server.
|
||||
You can read more about this feature in [the RFC](https://github.com/hasura/graphql-engine/blob/master/rfcs/apollo-federation.md).
|
||||
|
||||
This is an experimental feature (can be enabled by setting
|
||||
`HASURA_GRAPHQL_EXPERIMENTAL_FEATURES: apollo_federation`). This is supported
|
||||
over all databases. To expose a table in an Apollo federated gateway, we need
|
||||
to enable Apollo federation in its metadata. This can be done via the
|
||||
`*_track_table` metadata API and console support will be added soon.
|
||||
|
||||
For example, given a table called `user` in a database which is not being
|
||||
tracked by Hasura, we can run `*_track_table` to enable Apollo federation for
|
||||
the table:
|
||||
|
||||
```
|
||||
POST /v1/metadata HTTP/1.1
|
||||
Content-Type: application/json
|
||||
X-Hasura-Role: admin
|
||||
```
|
||||
``` json
|
||||
{
|
||||
"type": "pg_track_table",
|
||||
"args": {
|
||||
"table": "user",
|
||||
"schema": "public",
|
||||
"apollo_federation_config": {
|
||||
"enable": "v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
The above API call would add the `@key` directive in the GraphQL schema with
|
||||
fields argument set to the primary key of the table (say `id`), i.e:
|
||||
```graphql
|
||||
type user @key(fields: "id") {
|
||||
id: Int!
|
||||
name: String
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- server: When providing a JSON path in a JWT claims map, you can now use
|
||||
|
@ -526,8 +526,8 @@ elif [ "$MODE" = "test" ]; then
|
||||
# are defined.
|
||||
export HASURA_GRAPHQL_PG_SOURCE_URL_1=${HASURA_GRAPHQL_PG_SOURCE_URL_1-$PG_DB_URL}
|
||||
export HASURA_GRAPHQL_PG_SOURCE_URL_2=${HASURA_GRAPHQL_PG_SOURCE_URL_2-$PG_DB_URL}
|
||||
export HASURA_GRAPHQL_EXPERIMENTAL_FEATURES="inherited_roles, naming_convention"
|
||||
export HASURA_GRAPHQL_MSSQL_SOURCE_URL=$MSSQL_CONN_STR
|
||||
export HGE_URL="http://127.0.0.1:$HASURA_GRAPHQL_SERVER_PORT"
|
||||
|
||||
# Using --metadata-database-url flag to test multiple backends
|
||||
# HASURA_GRAPHQL_PG_SOURCE_URL_* For a couple multi-source pytests:
|
||||
|
@ -762,6 +762,7 @@ library
|
||||
, Hasura.RQL.IR.Root
|
||||
, Hasura.RQL.IR
|
||||
, Hasura.GraphQL.Analyse
|
||||
, Hasura.GraphQL.ApolloFederation
|
||||
, Hasura.GraphQL.Context
|
||||
, Hasura.GraphQL.Execute
|
||||
, Hasura.GraphQL.Execute.Action
|
||||
|
@ -26,6 +26,7 @@ module Data.Aeson.Ordered
|
||||
Data.Aeson.Ordered.lookup,
|
||||
toOrdered,
|
||||
fromOrdered,
|
||||
fromOrderedHashMap,
|
||||
)
|
||||
where
|
||||
|
||||
@ -102,6 +103,10 @@ safeUnion (Object_ x) (Object_ y) =
|
||||
empty :: Object
|
||||
empty = Object_ mempty
|
||||
|
||||
-- | Ordered Value from ordered hashmap
|
||||
fromOrderedHashMap :: InsOrdHashMap Text Value -> Value
|
||||
fromOrderedHashMap = Object . Object_
|
||||
|
||||
-- | Insert before the element at index i. Think of it in terms of
|
||||
-- 'splitAt', which is (take k, drop k). Deletes existing key, if any.
|
||||
insert :: (Int, Text) -> Value -> Object -> Object
|
||||
|
@ -208,11 +208,11 @@ bqColumnParser columnType (G.Nullability isNullable) =
|
||||
| otherwise = id
|
||||
mkEnumValue :: (EnumValue, EnumValueInfo) -> (P.Definition P.EnumValueInfo, ScalarValue 'BigQuery)
|
||||
mkEnumValue (EnumValue value, EnumValueInfo description) =
|
||||
( P.Definition value (G.Description <$> description) Nothing P.EnumValueInfo,
|
||||
( P.Definition value (G.Description <$> description) Nothing [] P.EnumValueInfo,
|
||||
BigQuery.StringValue $ G.unName value
|
||||
)
|
||||
throughJSON scalarName =
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition scalarName Nothing Nothing P.TIScalar
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition scalarName Nothing Nothing [] P.TIScalar
|
||||
in P.Parser
|
||||
{ pType = schemaType,
|
||||
pParser =
|
||||
@ -221,7 +221,7 @@ bqColumnParser columnType (G.Nullability isNullable) =
|
||||
}
|
||||
stringBased :: MonadParse m => G.Name -> Parser 'Both m Text
|
||||
stringBased scalarName =
|
||||
P.string {P.pType = P.TNamed P.NonNullable $ P.Definition scalarName Nothing Nothing P.TIScalar}
|
||||
P.string {P.pType = P.TNamed P.NonNullable $ P.Definition scalarName Nothing Nothing [] P.TIScalar}
|
||||
|
||||
bqScalarSelectionArgumentsParser ::
|
||||
MonadParse n =>
|
||||
@ -261,7 +261,7 @@ bqOrderByOperators _tCase =
|
||||
)
|
||||
]
|
||||
where
|
||||
define name desc = P.Definition name (Just desc) Nothing P.EnumValueInfo
|
||||
define name desc = P.Definition name (Just desc) Nothing [] P.EnumValueInfo
|
||||
|
||||
bqComparisonExps ::
|
||||
forall m n r.
|
||||
|
@ -125,7 +125,7 @@ orderByOperators' RQL.SourceInfo {_siConfiguration} _tCase =
|
||||
)
|
||||
]
|
||||
where
|
||||
define name desc = P.Definition name (Just desc) Nothing P.EnumValueInfo
|
||||
define name desc = P.Definition name (Just desc) Nothing [] P.EnumValueInfo
|
||||
|
||||
comparisonExps' ::
|
||||
forall m n r.
|
||||
|
@ -283,7 +283,7 @@ msColumnParser columnType (G.Nullability isNullable) =
|
||||
MSSQL.BitType -> pure $ ODBC.BoolValue <$> P.boolean
|
||||
_ -> do
|
||||
name <- MSSQL.mkMSSQLScalarTypeName scalarType
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing P.TIScalar
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing [] P.TIScalar
|
||||
pure $
|
||||
P.Parser
|
||||
{ pType = schemaType,
|
||||
@ -303,7 +303,7 @@ msColumnParser columnType (G.Nullability isNullable) =
|
||||
| otherwise = id
|
||||
mkEnumValue :: (EnumValue, EnumValueInfo) -> (P.Definition P.EnumValueInfo, ScalarValue 'MSSQL)
|
||||
mkEnumValue (EnumValue value, EnumValueInfo description) =
|
||||
( P.Definition value (G.Description <$> description) Nothing P.EnumValueInfo,
|
||||
( P.Definition value (G.Description <$> description) Nothing [] P.EnumValueInfo,
|
||||
ODBC.TextValue $ G.unName value
|
||||
)
|
||||
|
||||
@ -345,7 +345,7 @@ msOrderByOperators _tCase =
|
||||
)
|
||||
]
|
||||
where
|
||||
define name desc = P.Definition name (Just desc) Nothing P.EnumValueInfo
|
||||
define name desc = P.Definition name (Just desc) Nothing [] P.EnumValueInfo
|
||||
|
||||
msComparisonExps ::
|
||||
forall m n r.
|
||||
|
@ -132,7 +132,7 @@ tableInsertMatchColumnsEnum sourceInfo tableInfo = do
|
||||
]
|
||||
where
|
||||
define name =
|
||||
P.Definition name (Just $ G.Description "column name") Nothing P.EnumValueInfo
|
||||
P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
||||
|
||||
-- | Check whether a column can be used for match_columns.
|
||||
isMatchColumnValid :: ColumnInfo 'MSSQL -> Bool
|
||||
|
@ -196,7 +196,7 @@ columnParser' columnType (GQL.Nullability isNullable) =
|
||||
MySQL.Timestamp -> pure $ possiblyNullable scalarType $ MySQL.TimestampValue <$> P.string
|
||||
_ -> do
|
||||
name <- MySQL.mkMySQLScalarTypeName scalarType
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing P.TIScalar
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing [] P.TIScalar
|
||||
pure $
|
||||
P.Parser
|
||||
{ pType = schemaType,
|
||||
@ -217,7 +217,7 @@ columnParser' columnType (GQL.Nullability isNullable) =
|
||||
| otherwise = id
|
||||
mkEnumValue :: (EnumValue, EnumValueInfo) -> (P.Definition P.EnumValueInfo, RQL.ScalarValue 'MySQL)
|
||||
mkEnumValue (RQL.EnumValue value, EnumValueInfo description) =
|
||||
( P.Definition value (GQL.Description <$> description) Nothing P.EnumValueInfo,
|
||||
( P.Definition value (GQL.Description <$> description) Nothing [] P.EnumValueInfo,
|
||||
MySQL.VarcharValue $ GQL.unName value
|
||||
)
|
||||
|
||||
@ -252,7 +252,7 @@ orderByOperators' _tCase =
|
||||
)
|
||||
]
|
||||
where
|
||||
define name desc = P.Definition name (Just desc) Nothing P.EnumValueInfo
|
||||
define name desc = P.Definition name (Just desc) Nothing [] P.EnumValueInfo
|
||||
|
||||
-- | TODO: Make this as thorough as the one for MSSQL/PostgreSQL
|
||||
comparisonExps' ::
|
||||
|
@ -368,7 +368,7 @@ columnParser columnType (G.Nullability isNullable) = do
|
||||
--
|
||||
-- TODO: introduce new dedicated scalars for Postgres column types.
|
||||
name <- mkScalarTypeName scalarType
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing P.TIScalar
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing [] P.TIScalar
|
||||
pure $
|
||||
P.Parser
|
||||
{ pType = schemaType,
|
||||
@ -392,7 +392,7 @@ columnParser columnType (G.Nullability isNullable) = do
|
||||
| otherwise = id
|
||||
mkEnumValue :: NamingCase -> (EnumValue, EnumValueInfo) -> (P.Definition P.EnumValueInfo, PGScalarValue)
|
||||
mkEnumValue tCase (EnumValue value, EnumValueInfo description) =
|
||||
( P.Definition (applyEnumValueCase tCase value) (G.Description <$> description) Nothing P.EnumValueInfo,
|
||||
( P.Definition (applyEnumValueCase tCase value) (G.Description <$> description) Nothing [] P.EnumValueInfo,
|
||||
PGValText $ G.unName value
|
||||
)
|
||||
|
||||
@ -450,7 +450,7 @@ orderByOperators tCase =
|
||||
)
|
||||
]
|
||||
where
|
||||
define name desc = P.Definition name (Just desc) Nothing P.EnumValueInfo
|
||||
define name desc = P.Definition name (Just desc) Nothing [] P.EnumValueInfo
|
||||
|
||||
comparisonExps ::
|
||||
forall pgKind m n r.
|
||||
|
@ -147,6 +147,7 @@ conflictConstraint constraints sourceInfo tableInfo =
|
||||
name
|
||||
(Just $ "unique or primary key constraint on columns " <> coerce (showPGCols (HS.toList cCols)))
|
||||
Nothing
|
||||
[]
|
||||
P.EnumValueInfo,
|
||||
c
|
||||
)
|
||||
|
284
server/src-lib/Hasura/GraphQL/ApolloFederation.hs
Normal file
284
server/src-lib/Hasura/GraphQL/ApolloFederation.hs
Normal file
@ -0,0 +1,284 @@
|
||||
-- | Tools for generating fields for Apollo federation
|
||||
module Hasura.GraphQL.ApolloFederation
|
||||
( -- * Field Parser generators
|
||||
mkEntityUnionFieldParser,
|
||||
mkServiceField,
|
||||
apolloRootFields,
|
||||
ApolloFederationParserFunction (..),
|
||||
convertToApolloFedParserFunc,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens ((??))
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Key qualified as K
|
||||
import Data.Aeson.KeyMap qualified as KMap
|
||||
import Data.Aeson.Ordered qualified as JO
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||
import Data.HashSet qualified as Set
|
||||
import Data.Text qualified as T
|
||||
import Hasura.Base.Error
|
||||
import Hasura.Base.ErrorMessage (toErrorMessage)
|
||||
import Hasura.GraphQL.Parser qualified as P
|
||||
import Hasura.GraphQL.Schema.Common
|
||||
import Hasura.GraphQL.Schema.NamingCase
|
||||
import Hasura.GraphQL.Schema.Options (StringifyNumbers)
|
||||
import Hasura.GraphQL.Schema.Parser
|
||||
import Hasura.Name qualified as Name
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR qualified as IR
|
||||
import Hasura.RQL.IR.Root
|
||||
import Hasura.RQL.IR.Select
|
||||
import Hasura.RQL.IR.Value (UnpreparedValue, ValueWithOrigin (ValueNoOrigin))
|
||||
import Hasura.RQL.Types.Backend
|
||||
import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Source
|
||||
import Hasura.RQL.Types.Table
|
||||
import Hasura.SQL.AnyBackend qualified as AB
|
||||
import Hasura.Server.Types
|
||||
import Language.GraphQL.Draft.Printer qualified as Printer
|
||||
import Language.GraphQL.Draft.Syntax qualified as G
|
||||
import Text.Builder qualified as Builder
|
||||
|
||||
-- | Internal parser function for entities field
|
||||
data ApolloFederationParserFunction n = ApolloFederationParserFunction
|
||||
{ aafuGetRootField :: ApolloFederationAnyType -> n (QueryRootField UnpreparedValue)
|
||||
}
|
||||
|
||||
-- | Haskell representation of _Any scalar
|
||||
data ApolloFederationAnyType = ApolloFederationAnyType
|
||||
{ afTypename :: G.Name,
|
||||
afPKValues :: J.Object
|
||||
}
|
||||
deriving stock (Show)
|
||||
|
||||
-- | Parser for _Any scalar
|
||||
anyParser :: P.Parser origin 'Both Parse ApolloFederationAnyType
|
||||
anyParser =
|
||||
jsonScalar Name.__Any (Just "Scalar _Any") `bind` \val -> do
|
||||
let typenameKey = K.fromText "__typename"
|
||||
case val of
|
||||
J.Object obj -> case KMap.lookup typenameKey obj of
|
||||
Just (J.String txt) -> case G.mkName txt of
|
||||
Just tName ->
|
||||
pure $
|
||||
ApolloFederationAnyType
|
||||
{ afTypename = tName,
|
||||
afPKValues = KMap.delete typenameKey obj
|
||||
}
|
||||
Nothing -> P.parseError $ toErrorMessage $ txt <> " is not a valid graphql name"
|
||||
Nothing -> P.parseError $ toErrorMessage "__typename key not found"
|
||||
_ -> P.parseError $ toErrorMessage "__typename can only be a string value"
|
||||
_ -> P.parseError $ toErrorMessage "representations is expecting a list of objects only"
|
||||
|
||||
convertToApolloFedParserFunc ::
|
||||
(Monad n, MonadParse n, Backend b) =>
|
||||
SourceInfo b ->
|
||||
TableInfo b ->
|
||||
TablePermG b (UnpreparedValue b) ->
|
||||
StringifyNumbers ->
|
||||
Maybe NamingCase ->
|
||||
NESeq (ColumnInfo b) ->
|
||||
Parser 'Output n (AnnotatedFields b) ->
|
||||
Parser 'Output n (ApolloFederationParserFunction n)
|
||||
convertToApolloFedParserFunc sInfo tInfo selPerm stringifyNumbers tCase pKeys =
|
||||
fmap (modifyApolloFedParserFunc sInfo tInfo selPerm stringifyNumbers tCase pKeys)
|
||||
|
||||
modifyApolloFedParserFunc ::
|
||||
(MonadParse n, Backend b) =>
|
||||
SourceInfo b ->
|
||||
TableInfo b ->
|
||||
TablePermG b (UnpreparedValue b) ->
|
||||
StringifyNumbers ->
|
||||
Maybe NamingCase ->
|
||||
NESeq (ColumnInfo b) ->
|
||||
AnnotatedFields b ->
|
||||
ApolloFederationParserFunction n
|
||||
modifyApolloFedParserFunc
|
||||
SourceInfo {..}
|
||||
TableInfo {..}
|
||||
selectPermissions
|
||||
stringifyNumbers
|
||||
tCase
|
||||
primaryKeys
|
||||
annField = ApolloFederationParserFunction $ \ApolloFederationAnyType {..} -> do
|
||||
allConstraints <-
|
||||
for primaryKeys \columnInfo -> do
|
||||
let colName = G.unName $ ciName columnInfo
|
||||
cvType = ciType columnInfo
|
||||
cvValue <- case KMap.lookup (K.fromText colName) afPKValues of
|
||||
Nothing -> P.parseError . toErrorMessage $ "cannot find " <> colName <> " in _Any type"
|
||||
Just va -> liftQErr $ parseScalarValueColumnType (ciType columnInfo) va
|
||||
pure $
|
||||
IR.BoolField . IR.AVColumn columnInfo . pure . IR.AEQ True . IR.mkParameter $
|
||||
ValueNoOrigin $ ColumnValue {..}
|
||||
let whereExpr = Just $ IR.BoolAnd $ toList allConstraints
|
||||
sourceName = _siName
|
||||
sourceConfig = _siConfiguration
|
||||
tableName = _tciName _tiCoreInfo
|
||||
queryDBRoot =
|
||||
IR.QDBR $
|
||||
IR.QDBSingleRow $
|
||||
IR.AnnSelectG
|
||||
{ IR._asnFields = annField,
|
||||
IR._asnFrom = IR.FromTable tableName,
|
||||
IR._asnPerm = selectPermissions,
|
||||
IR._asnArgs = IR.noSelectArgs {IR._saWhere = whereExpr},
|
||||
IR._asnStrfyNum = stringifyNumbers,
|
||||
IR._asnNamingConvention = tCase
|
||||
}
|
||||
pure $
|
||||
IR.RFDB sourceName $
|
||||
AB.mkAnyBackend $
|
||||
IR.SourceConfigWith sourceConfig Nothing $
|
||||
queryDBRoot
|
||||
where
|
||||
liftQErr = either (P.parseError . toErrorMessage . qeError) pure . runExcept
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Related to @service@ field
|
||||
|
||||
-- main function
|
||||
|
||||
-- | Creates @_service@ @FieldParser@ using the schema introspection.
|
||||
-- This will allow us to process the following query:
|
||||
--
|
||||
-- > query {
|
||||
-- > _service {
|
||||
-- > sdl
|
||||
-- > }
|
||||
-- > }
|
||||
mkServiceField ::
|
||||
FieldParser P.Parse (G.SchemaIntrospection -> QueryRootField UnpreparedValue)
|
||||
mkServiceField = serviceFieldParser
|
||||
where
|
||||
sdlField = JO.String . generateSDL <$ P.selection_ Name._sdl (Just "SDL representation of schema") P.string
|
||||
serviceParser = P.nonNullableParser $ P.selectionSet Name.__Service Nothing [sdlField]
|
||||
serviceFieldParser =
|
||||
P.subselection_ Name.__service Nothing serviceParser `bindField` \selSet -> do
|
||||
let partialValue = OMap.map (\ps -> handleTypename (\tName _ -> JO.toOrdered tName) ps) (OMap.mapKeys G.unName selSet)
|
||||
pure \schemaIntrospection -> RFRaw . JO.fromOrderedHashMap $ (partialValue ?? schemaIntrospection)
|
||||
|
||||
apolloRootFields ::
|
||||
Set.HashSet ExperimentalFeature ->
|
||||
[(G.Name, Parser 'Output P.Parse (ApolloFederationParserFunction P.Parse))] ->
|
||||
[FieldParser P.Parse (G.SchemaIntrospection -> QueryRootField UnpreparedValue)]
|
||||
apolloRootFields expFeatures apolloFedTableParsers =
|
||||
let -- generate the `_service` field parser
|
||||
serviceField = mkServiceField
|
||||
|
||||
-- generate the `_entities` field parser
|
||||
entityField = const <$> mkEntityUnionFieldParser apolloFedTableParsers
|
||||
in -- we would want to expose these fields inorder to support apollo federation
|
||||
-- refer https://www.apollographql.com/docs/federation/federation-spec
|
||||
-- `serviceField` is essential to connect hasura to gateway, `entityField`
|
||||
-- is essential only if we have types that has @key directive
|
||||
if
|
||||
| EFApolloFederation `elem` expFeatures && not (null apolloFedTableParsers) ->
|
||||
[serviceField, entityField]
|
||||
| EFApolloFederation `elem` expFeatures ->
|
||||
[serviceField]
|
||||
| otherwise -> []
|
||||
|
||||
-- helpers
|
||||
|
||||
-- | Generate sdl from the schema introspection
|
||||
generateSDL :: G.SchemaIntrospection -> Text
|
||||
generateSDL (G.SchemaIntrospection sIntro) = sdl
|
||||
where
|
||||
-- NOTE: add this to the sdl to support apollo v2 directive
|
||||
_supportV2 :: Text
|
||||
_supportV2 = "\n\nextend schema\n@link(url: \"https://specs.apollo.dev/federation/v2.0\",\nimport: [\"@key\", \"@shareable\"])"
|
||||
|
||||
-- first we filter out the type definitions which are not relevent such as
|
||||
-- schema fields and types (starts with `__`)
|
||||
typeDefns = mapMaybe filterAndWrapTypeSystemDefinition (Map.elems sIntro)
|
||||
|
||||
-- next we get the root operation type definitions
|
||||
rootOpTypeDefns =
|
||||
mapMaybe
|
||||
( \(fieldName, operationType) ->
|
||||
Map.lookup fieldName sIntro
|
||||
$> G.RootOperationTypeDefinition operationType fieldName
|
||||
)
|
||||
[ (Name._query_root, G.OperationTypeQuery),
|
||||
(Name._mutation_root, G.OperationTypeMutation),
|
||||
(Name._subscription_root, G.OperationTypeSubscription)
|
||||
]
|
||||
|
||||
-- finally we gather everything, run the printer and generate full sdl in `Text`
|
||||
sdl = Builder.run $ Printer.schemaDocument getSchemaDocument
|
||||
|
||||
getSchemaDocument :: G.SchemaDocument
|
||||
getSchemaDocument =
|
||||
G.SchemaDocument $
|
||||
G.TypeSystemDefinitionSchema (G.SchemaDefinition Nothing (rootOpTypeDefns)) : typeDefns
|
||||
|
||||
-- | Filter out schema components from sdl which are not required by apollo federation and
|
||||
-- wraps it in `TypeSystemDefinition`
|
||||
filterAndWrapTypeSystemDefinition :: G.TypeDefinition [G.Name] G.InputValueDefinition -> Maybe G.TypeSystemDefinition
|
||||
filterAndWrapTypeSystemDefinition = \case
|
||||
G.TypeDefinitionScalar (G.ScalarTypeDefinition {}) -> Nothing
|
||||
G.TypeDefinitionInterface (G.InterfaceTypeDefinition a b c d _) ->
|
||||
Just $ G.TypeSystemDefinitionType (G.TypeDefinitionInterface (G.InterfaceTypeDefinition a b c d ()))
|
||||
G.TypeDefinitionObject (G.ObjectTypeDefinition a b c d e) ->
|
||||
-- We are skipping the schema types here
|
||||
Just . G.TypeSystemDefinitionType . G.TypeDefinitionObject $
|
||||
G.ObjectTypeDefinition a b c d (filter (not . T.isPrefixOf "__" . G.unName . G._fldName) e)
|
||||
G.TypeDefinitionUnion defn -> Just $ G.TypeSystemDefinitionType (G.TypeDefinitionUnion defn)
|
||||
G.TypeDefinitionEnum defn -> Just $ G.TypeSystemDefinitionType (G.TypeDefinitionEnum defn)
|
||||
G.TypeDefinitionInputObject defn -> Just $ G.TypeSystemDefinitionType (G.TypeDefinitionInputObject defn)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Related to @_entities@ field
|
||||
|
||||
-- main function
|
||||
|
||||
-- | Creates @_entities@ @FieldParser@ using `Parser`s for Entity union, schema
|
||||
-- introspection and a list of all query `FieldParser`.
|
||||
-- This will allow us to process the following query:
|
||||
--
|
||||
-- > query ($representations: [_Any!]!) {
|
||||
-- > _entities(representations: $representations) {
|
||||
-- > ... on SomeType {
|
||||
-- > foo
|
||||
-- > bar
|
||||
-- > }
|
||||
-- > }
|
||||
-- > }
|
||||
mkEntityUnionFieldParser ::
|
||||
[(G.Name, Parser 'Output Parse (ApolloFederationParserFunction Parse))] ->
|
||||
FieldParser P.Parse (QueryRootField UnpreparedValue)
|
||||
mkEntityUnionFieldParser apolloFedTableParsers =
|
||||
let entityParserMap = Map.fromList apolloFedTableParsers
|
||||
|
||||
-- the Union `Entities`
|
||||
bodyParser = P.selectionSetUnion Name.__Entity (Just "A union of all types that use the @key directive") entityParserMap
|
||||
|
||||
-- name of the field
|
||||
name = Name.__entities
|
||||
|
||||
-- description of the field
|
||||
description = Just "query _Entity union"
|
||||
|
||||
representationParser =
|
||||
field Name._representations Nothing $ list $ anyParser
|
||||
|
||||
entityParser =
|
||||
subselection name description representationParser bodyParser
|
||||
`bindField` \(parsedArgs, parsedBody) -> do
|
||||
rootFields <-
|
||||
for
|
||||
parsedArgs
|
||||
( \anyArg ->
|
||||
case Map.lookup (afTypename anyArg) parsedBody of
|
||||
Nothing -> (P.parseError . toErrorMessage) $ G.unName (afTypename anyArg) <> " is not found in selection set or apollo federation is not enabled for the type"
|
||||
Just aafus -> (aafuGetRootField aafus) anyArg
|
||||
)
|
||||
pure $ concatQueryRootFields rootFields
|
||||
in entityParser
|
||||
|
||||
-- | concatenates multiple fields
|
||||
concatQueryRootFields :: [QueryRootField UnpreparedValue] -> QueryRootField UnpreparedValue
|
||||
concatQueryRootFields = RFMulti
|
@ -217,6 +217,7 @@ buildSubscriptionPlan userInfo rootFields parameterizedQueryHash = do
|
||||
go (accLiveQueryFields, accStreamingFields) (gName, field) = case field of
|
||||
IR.RFRemote _ -> throw400 NotSupported "subscription to remote server is not supported"
|
||||
IR.RFRaw _ -> throw400 NotSupported "Introspection not supported over subscriptions"
|
||||
IR.RFMulti _ -> throw400 NotSupported "not supported over subscriptions"
|
||||
IR.RFDB src e -> do
|
||||
let subscriptionType =
|
||||
case AB.unpackAnyBackend @('Postgres 'Vanilla) e of
|
||||
|
@ -261,6 +261,9 @@ data ExecutionStep where
|
||||
ExecStepRaw ::
|
||||
JO.Value ->
|
||||
ExecutionStep
|
||||
ExecStepMulti ::
|
||||
[ExecutionStep] ->
|
||||
ExecutionStep
|
||||
|
||||
-- | The series of steps that need to be executed for a given query. For now, those steps are all
|
||||
-- independent. In the future, when we implement a client-side dataloader and generalized joins,
|
||||
|
@ -117,30 +117,35 @@ convertMutationSelectionSet
|
||||
|
||||
let parameterizedQueryHash = calculateParameterizedQueryHash resolvedSelSet
|
||||
|
||||
resolveExecutionSteps rootFieldName rootFieldUnpreparedValue = do
|
||||
case rootFieldUnpreparedValue of
|
||||
RFDB sourceName exists ->
|
||||
AB.dispatchAnyBackend @BackendExecute
|
||||
exists
|
||||
\(SourceConfigWith (sourceConfig :: SourceConfig b) queryTagsConfig (MDBR db)) -> do
|
||||
let mutationQueryTagsAttributes = encodeQueryTags $ QTMutation $ MutationMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
||||
queryTagsComment = Tagged.untag $ createQueryTags @m mutationQueryTagsAttributes queryTagsConfig
|
||||
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsMutationDB db
|
||||
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBMutationPlan @b userInfo stringifyNum sourceName sourceConfig noRelsDBAST
|
||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||
RFRemote remoteField -> do
|
||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
||||
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField resolvedRemoteField
|
||||
pure $
|
||||
buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeMutation noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||
RFAction action -> do
|
||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionMutation action
|
||||
(actionName, _fch) <- pure $ case noRelsDBAST of
|
||||
AMSync s -> (_aaeName s, _aaeForwardClientHeaders s)
|
||||
AMAsync s -> (_aamaName s, _aamaForwardClientHeaders s)
|
||||
plan <- convertMutationAction env logger userInfo manager reqHeaders (Just (GH._grQuery gqlUnparsed)) noRelsDBAST
|
||||
pure $ ExecStepAction plan (ActionsInfo actionName _fch) remoteJoins -- `_fch` represents the `forward_client_headers` option from the action
|
||||
-- definition which is currently being ignored for actions that are mutations
|
||||
RFRaw customFieldVal -> flip onLeft throwError =<< executeIntrospection userInfo customFieldVal introspectionDisabledRoles
|
||||
RFMulti lst -> do
|
||||
allSteps <- traverse (resolveExecutionSteps rootFieldName) lst
|
||||
pure $ ExecStepMulti allSteps
|
||||
|
||||
-- Transform the RQL AST into a prepared SQL query
|
||||
txs <- flip OMap.traverseWithKey unpreparedQueries $ \rootFieldName rootFieldUnpreparedValue -> do
|
||||
case rootFieldUnpreparedValue of
|
||||
RFDB sourceName exists ->
|
||||
AB.dispatchAnyBackend @BackendExecute
|
||||
exists
|
||||
\(SourceConfigWith (sourceConfig :: SourceConfig b) queryTagsConfig (MDBR db)) -> do
|
||||
let mutationQueryTagsAttributes = encodeQueryTags $ QTMutation $ MutationMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
||||
queryTagsComment = Tagged.untag $ createQueryTags @m mutationQueryTagsAttributes queryTagsConfig
|
||||
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsMutationDB db
|
||||
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBMutationPlan @b userInfo stringifyNum sourceName sourceConfig noRelsDBAST
|
||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||
RFRemote remoteField -> do
|
||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
||||
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField resolvedRemoteField
|
||||
pure $
|
||||
buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeMutation noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||
RFAction action -> do
|
||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionMutation action
|
||||
(actionName, _fch) <- pure $ case noRelsDBAST of
|
||||
AMSync s -> (_aaeName s, _aaeForwardClientHeaders s)
|
||||
AMAsync s -> (_aamaName s, _aamaForwardClientHeaders s)
|
||||
plan <- convertMutationAction env logger userInfo manager reqHeaders (Just (GH._grQuery gqlUnparsed)) noRelsDBAST
|
||||
pure $ ExecStepAction plan (ActionsInfo actionName _fch) remoteJoins -- `_fch` represents the `forward_client_headers` option from the action
|
||||
-- definition which is currently being ignored for actions that are mutations
|
||||
RFRaw s -> flip onLeft throwError =<< executeIntrospection userInfo s introspectionDisabledRoles
|
||||
txs <- flip OMap.traverseWithKey unpreparedQueries $ resolveExecutionSteps
|
||||
return (txs, parameterizedQueryHash)
|
||||
|
@ -101,27 +101,31 @@ convertQuerySelSet
|
||||
|
||||
let parameterizedQueryHash = calculateParameterizedQueryHash normalizedSelectionSet
|
||||
|
||||
resolveExecutionSteps rootFieldName rootFieldUnpreparedValue = do
|
||||
case rootFieldUnpreparedValue of
|
||||
RFMulti lst -> do
|
||||
allSteps <- traverse (resolveExecutionSteps rootFieldName) lst
|
||||
pure $ ExecStepMulti allSteps
|
||||
RFDB sourceName exists ->
|
||||
AB.dispatchAnyBackend @BackendExecute
|
||||
exists
|
||||
\(SourceConfigWith (sourceConfig :: (SourceConfig b)) queryTagsConfig (QDBR db)) -> do
|
||||
let queryTagsAttributes = encodeQueryTags $ QTQuery $ QueryMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
||||
queryTagsComment = Tagged.untag $ createQueryTags @m queryTagsAttributes queryTagsConfig
|
||||
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsQueryDB db
|
||||
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBQueryPlan @b userInfo env sourceName sourceConfig noRelsDBAST
|
||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||
RFRemote rf -> do
|
||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer remoteField <- runVariableCache $ for rf $ resolveRemoteVariable userInfo
|
||||
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField remoteField
|
||||
pure $ buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeQuery noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||
RFAction action -> do
|
||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionQuery action
|
||||
(actionExecution, actionName, fch) <- pure $ case noRelsDBAST of
|
||||
AQQuery s -> (AEPSync $ resolveActionExecution env logger userInfo s (ActionExecContext manager reqHeaders (_uiSession userInfo)) (Just (GH._grQuery gqlUnparsed)), _aaeName s, _aaeForwardClientHeaders s)
|
||||
AQAsync s -> (AEPAsyncQuery $ AsyncActionQueryExecutionPlan (_aaaqActionId s) $ resolveAsyncActionQuery userInfo s, _aaaqName s, _aaaqForwardClientHeaders s)
|
||||
pure $ ExecStepAction actionExecution (ActionsInfo actionName fch) remoteJoins
|
||||
RFRaw r -> flip onLeft throwError =<< executeIntrospection userInfo r introspectionDisabledRoles
|
||||
-- 3. Transform the 'RootFieldMap' into an execution plan
|
||||
executionPlan <- flip OMap.traverseWithKey unpreparedQueries $ \rootFieldName rootFieldUnpreparedValue -> do
|
||||
case rootFieldUnpreparedValue of
|
||||
RFDB sourceName exists ->
|
||||
AB.dispatchAnyBackend @BackendExecute
|
||||
exists
|
||||
\(SourceConfigWith (sourceConfig :: (SourceConfig b)) queryTagsConfig (QDBR db)) -> do
|
||||
let queryTagsAttributes = encodeQueryTags $ QTQuery $ QueryMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
||||
queryTagsComment = Tagged.untag $ createQueryTags @m queryTagsAttributes queryTagsConfig
|
||||
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsQueryDB db
|
||||
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBQueryPlan @b userInfo env sourceName sourceConfig noRelsDBAST
|
||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||
RFRemote rf -> do
|
||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer remoteField <- runVariableCache $ for rf $ resolveRemoteVariable userInfo
|
||||
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField remoteField
|
||||
pure $ buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeQuery noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||
RFAction action -> do
|
||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionQuery action
|
||||
(actionExecution, actionName, fch) <- pure $ case noRelsDBAST of
|
||||
AQQuery s -> (AEPSync $ resolveActionExecution env logger userInfo s (ActionExecContext manager reqHeaders (_uiSession userInfo)) (Just (GH._grQuery gqlUnparsed)), _aaeName s, _aaeForwardClientHeaders s)
|
||||
AQAsync s -> (AEPAsyncQuery $ AsyncActionQueryExecutionPlan (_aaaqActionId s) $ resolveAsyncActionQuery userInfo s, _aaaqName s, _aaaqForwardClientHeaders s)
|
||||
pure $ ExecStepAction actionExecution (ActionsInfo actionName fch) remoteJoins
|
||||
RFRaw r -> flip onLeft throwError =<< executeIntrospection userInfo r introspectionDisabledRoles
|
||||
executionPlan <- flip OMap.traverseWithKey unpreparedQueries $ resolveExecutionSteps
|
||||
pure (executionPlan, OMap.elems unpreparedQueries, dirMap, parameterizedQueryHash)
|
||||
|
@ -61,6 +61,7 @@ explainQueryField userInfo fieldName rootField = do
|
||||
RFRemote _ -> throw400 InvalidParams "only hasura queries can be explained"
|
||||
RFAction _ -> throw400 InvalidParams "query actions cannot be explained"
|
||||
RFRaw _ -> pure $ encJFromJValue $ ExplainPlan fieldName Nothing Nothing
|
||||
RFMulti _ -> pure $ encJFromJValue $ ExplainPlan fieldName Nothing Nothing
|
||||
RFDB sourceName exists -> do
|
||||
step <- AB.dispatchAnyBackend @BackendExecute
|
||||
exists
|
||||
|
@ -242,7 +242,7 @@ field ::
|
||||
InputFieldsParser origin m a
|
||||
field name description parser =
|
||||
InputFieldsParser
|
||||
{ ifDefinitions = [Definition name description Nothing $ InputFieldInfo (pType parser) Nothing],
|
||||
{ ifDefinitions = [Definition name description Nothing [] $ InputFieldInfo (pType parser) Nothing],
|
||||
ifParser = \values -> withKey (A.Key (K.fromText (unName name))) do
|
||||
value <-
|
||||
maybe (parseError ("missing required field " <> toErrorValue name)) pure $ M.lookup name values <|> nullableDefault
|
||||
@ -271,7 +271,7 @@ fieldOptional ::
|
||||
fieldOptional name description parser =
|
||||
InputFieldsParser
|
||||
{ ifDefinitions =
|
||||
[ Definition name description Nothing $
|
||||
[ Definition name description Nothing [] $
|
||||
InputFieldInfo (nullableType $ pType parser) Nothing
|
||||
],
|
||||
ifParser =
|
||||
@ -295,7 +295,7 @@ fieldWithDefault ::
|
||||
InputFieldsParser origin m a
|
||||
fieldWithDefault name description defaultValue parser =
|
||||
InputFieldsParser
|
||||
{ ifDefinitions = [Definition name description Nothing $ InputFieldInfo (pType parser) (Just defaultValue)],
|
||||
{ ifDefinitions = [Definition name description Nothing [] $ InputFieldInfo (pType parser) (Just defaultValue)],
|
||||
ifParser =
|
||||
M.lookup name
|
||||
>>> withKey (A.Key (K.fromText (unName name))) . \case
|
||||
@ -325,7 +325,7 @@ enum name description values =
|
||||
other -> typeMismatch name "an enum value" other
|
||||
}
|
||||
where
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing $ TIEnum (fst <$> values)
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing [] $ TIEnum (fst <$> values)
|
||||
valuesMap = M.fromList $ over (traverse . _1) dName $ NonEmpty.toList values
|
||||
validate value =
|
||||
maybe invalidType pure $ M.lookup value valuesMap
|
||||
@ -369,7 +369,7 @@ object name description parser =
|
||||
where
|
||||
schemaType =
|
||||
TNamed NonNullable $
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
TIInputObject (InputObjectInfo (ifDefinitions parser))
|
||||
fieldNames = S.fromList (dName <$> ifDefinitions parser)
|
||||
parseFields fields = do
|
||||
|
@ -30,6 +30,7 @@ import Data.Hashable (Hashable)
|
||||
import Data.Maybe qualified as Maybe
|
||||
import Data.Traversable (for)
|
||||
import Data.Type.Equality
|
||||
import Data.Void (Void)
|
||||
import Hasura.Base.Error
|
||||
import Hasura.Base.ErrorMessage
|
||||
import Hasura.Base.ToErrorValue
|
||||
@ -100,17 +101,17 @@ nullable parser =
|
||||
|
||||
-- | Decorate a schema field as NON_NULL
|
||||
nonNullableField :: forall m origin a. FieldParser origin m a -> FieldParser origin m a
|
||||
nonNullableField (FieldParser (Definition n d o (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o (FieldInfo as (nonNullableType t))) p
|
||||
nonNullableField (FieldParser (Definition n d o dLst (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o dLst (FieldInfo as (nonNullableType t))) p
|
||||
|
||||
-- | Decorate a schema field as NULL
|
||||
nullableField :: forall m origin a. FieldParser origin m a -> FieldParser origin m a
|
||||
nullableField (FieldParser (Definition n d o (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o (FieldInfo as (nullableType t))) p
|
||||
nullableField (FieldParser (Definition n d o dLst (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o dLst (FieldInfo as (nullableType t))) p
|
||||
|
||||
multipleField :: forall m origin a. FieldParser origin m a -> FieldParser origin m a
|
||||
multipleField (FieldParser (Definition n d o (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o (FieldInfo as (TList Nullable t))) p
|
||||
multipleField (FieldParser (Definition n d o dLst (FieldInfo as t)) p) =
|
||||
FieldParser (Definition n d o dLst (FieldInfo as (TList Nullable t))) p
|
||||
|
||||
-- | Decorate a schema field with reference to given @'G.GType'
|
||||
wrapFieldParser :: forall m origin a. G.GType -> FieldParser origin m a -> FieldParser origin m a
|
||||
@ -142,14 +143,33 @@ setParserOrigin o (Parser typ p) =
|
||||
|
||||
-- | Set the metadata origin of a 'FieldParser'
|
||||
setFieldParserOrigin :: forall m origin a. origin -> FieldParser origin m a -> FieldParser origin m a
|
||||
setFieldParserOrigin o (FieldParser (Definition n d _ i) p) =
|
||||
FieldParser (Definition n d (Just o) i) p
|
||||
setFieldParserOrigin o (FieldParser (Definition n d _ dLst i) p) =
|
||||
FieldParser (Definition n d (Just o) dLst i) p
|
||||
|
||||
-- | Set the metadata origin of the arguments in a 'InputFieldsParser'
|
||||
setInputFieldsParserOrigin :: forall m origin a. origin -> InputFieldsParser origin m a -> InputFieldsParser origin m a
|
||||
setInputFieldsParserOrigin o (InputFieldsParser defs p) =
|
||||
InputFieldsParser (map (setDefinitionOrigin o) defs) p
|
||||
|
||||
-- | Set the directives of a 'Definition'
|
||||
setDefinitionDirectives :: [G.Directive Void] -> Definition origin a -> Definition origin a
|
||||
setDefinitionDirectives dLst def = def {dDirectives = dLst}
|
||||
|
||||
-- | Set the directives of a 'Parser'
|
||||
setParserDirectives :: forall origin k m a. [G.Directive Void] -> Parser origin k m a -> Parser origin k m a
|
||||
setParserDirectives dLst (Parser typ p) =
|
||||
Parser (onTypeDef (setDefinitionDirectives dLst) typ) p
|
||||
|
||||
-- | Set the directives of a 'FieldParser'
|
||||
setFieldParserDirectives :: forall m origin a. [G.Directive Void] -> FieldParser origin m a -> FieldParser origin m a
|
||||
setFieldParserDirectives dLst (FieldParser (Definition n d o _ i) p) =
|
||||
FieldParser (Definition n d o dLst i) p
|
||||
|
||||
-- | Set the directives of the arguments in a 'InputFieldsParser'
|
||||
setInputFieldsParserDirectives :: forall m origin a. [G.Directive Void] -> InputFieldsParser origin m a -> InputFieldsParser origin m a
|
||||
setInputFieldsParserDirectives dLst (InputFieldsParser defs p) =
|
||||
InputFieldsParser (map (setDefinitionDirectives dLst) defs) p
|
||||
|
||||
-- | A variant of 'selectionSetObject' which doesn't implement any interfaces
|
||||
selectionSet ::
|
||||
MonadParse m =>
|
||||
@ -207,7 +227,7 @@ selectionSetObject name description parsers implementsInterfaces =
|
||||
Parser
|
||||
{ pType =
|
||||
TNamed Nullable $
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
TIObject $ ObjectInfo (map fDefinition parsers) interfaces,
|
||||
pParser = \input -> withKey (Key "selectionSet") do
|
||||
-- Not all fields have a selection set, but if they have one, it
|
||||
@ -263,7 +283,7 @@ selectionSetInterface name description fields objectImplementations =
|
||||
Parser
|
||||
{ pType =
|
||||
TNamed Nullable $
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
TIInterface $ InterfaceInfo (map fDefinition fields) objects,
|
||||
pParser = \input -> for objectImplementations (($ input) . pParser)
|
||||
-- Note: This is somewhat suboptimal, since it parses a query against every
|
||||
@ -290,7 +310,7 @@ selectionSetUnion name description objectImplementations =
|
||||
Parser
|
||||
{ pType =
|
||||
TNamed Nullable $
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
TIUnion $ UnionInfo objects,
|
||||
pParser = \input -> for objectImplementations (($ input) . pParser)
|
||||
}
|
||||
@ -330,7 +350,7 @@ rawSelection ::
|
||||
rawSelection name description argumentsParser resultParser =
|
||||
FieldParser
|
||||
{ fDefinition =
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
FieldInfo (ifDefinitions argumentsParser) (pType resultParser),
|
||||
fParser = \Field {_fAlias, _fArguments, _fSelectionSet} -> do
|
||||
unless (null _fSelectionSet) $
|
||||
@ -386,7 +406,7 @@ rawSubselection ::
|
||||
rawSubselection name description argumentsParser bodyParser =
|
||||
FieldParser
|
||||
{ fDefinition =
|
||||
Definition name description Nothing $
|
||||
Definition name description Nothing [] $
|
||||
FieldInfo (ifDefinitions argumentsParser) (pType bodyParser),
|
||||
fParser = \Field {_fAlias, _fArguments, _fSelectionSet} -> do
|
||||
-- check for extraneous arguments here, since the InputFieldsParser just
|
||||
|
@ -161,7 +161,7 @@ unsafeRawScalar ::
|
||||
Parser origin 'Both n (InputValue Variable)
|
||||
unsafeRawScalar name description =
|
||||
Parser
|
||||
{ pType = TNamed NonNullable $ Definition name description Nothing TIScalar,
|
||||
{ pType = TNamed NonNullable $ Definition name description Nothing [] TIScalar,
|
||||
pParser = pure
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ jsonScalar name description =
|
||||
pParser = valueToJSON $ toGraphQLType schemaType
|
||||
}
|
||||
where
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing TIScalar
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing [] TIScalar
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Local helpers
|
||||
@ -191,7 +191,7 @@ mkScalar name description parser =
|
||||
pParser = peelVariable (toGraphQLType schemaType) >=> parser
|
||||
}
|
||||
where
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing TIScalar
|
||||
schemaType = TNamed NonNullable $ Definition name description Nothing [] TIScalar
|
||||
|
||||
convertWith ::
|
||||
MonadParse m =>
|
||||
|
@ -570,12 +570,12 @@ getInterfaceInfo t = case getTypeInfo t of
|
||||
data SomeDefinitionTypeInfo origin = forall k. SomeDefinitionTypeInfo (Definition origin (TypeInfo origin k))
|
||||
|
||||
instance HasName (SomeDefinitionTypeInfo origin) where
|
||||
getName (SomeDefinitionTypeInfo (Definition n _ _ _)) = n
|
||||
getName (SomeDefinitionTypeInfo (Definition n _ _ _ _)) = n
|
||||
|
||||
instance Eq (SomeDefinitionTypeInfo origin) where
|
||||
-- Same as instance Eq Definition
|
||||
SomeDefinitionTypeInfo (Definition name1 _ _ ti1)
|
||||
== SomeDefinitionTypeInfo (Definition name2 _ _ ti2) =
|
||||
SomeDefinitionTypeInfo (Definition name1 _ _ _ ti1)
|
||||
== SomeDefinitionTypeInfo (Definition name2 _ _ _ ti2) =
|
||||
name1 == name2 && eqTypeInfo ti1 ti2
|
||||
|
||||
data Definition origin a = Definition
|
||||
@ -598,6 +598,8 @@ data Definition origin a = Definition
|
||||
-- Maybe, at some point, it makes sense to represent the above options more
|
||||
-- accurately in the type of 'dOrigin'.
|
||||
dOrigin :: Maybe origin,
|
||||
-- | The directives for this object.
|
||||
dDirectives :: [G.Directive Void],
|
||||
-- | Lazy to allow mutually-recursive type definitions.
|
||||
dInfo :: ~a
|
||||
}
|
||||
@ -613,8 +615,8 @@ instance Eq a => Eq (Definition origin a) where
|
||||
instance Eq1 (Definition origin) where
|
||||
liftEq
|
||||
eq
|
||||
(Definition name1 _ _ info1)
|
||||
(Definition name2 _ _ info2) =
|
||||
(Definition name1 _ _ _ info1)
|
||||
(Definition name2 _ _ _ info2) =
|
||||
name1 == name2 && eq info1 info2
|
||||
|
||||
instance HasName (Definition origin a) where
|
||||
|
@ -6,7 +6,6 @@ module Hasura.GraphQL.Parser.Schema.Convert
|
||||
where
|
||||
|
||||
import Data.List.NonEmpty qualified as NonEmpty
|
||||
import Data.Void (Void)
|
||||
import Hasura.GraphQL.Parser.Schema
|
||||
import Language.GraphQL.Draft.Syntax qualified as G
|
||||
import Prelude
|
||||
@ -27,14 +26,14 @@ convertType (SomeDefinitionTypeInfo Definition {..}) = case dInfo of
|
||||
G.ScalarTypeDefinition
|
||||
{ G._stdDescription = dDescription,
|
||||
G._stdName = dName,
|
||||
G._stdDirectives = noDirectives
|
||||
G._stdDirectives = dDirectives
|
||||
}
|
||||
TIEnum enumInfo ->
|
||||
G.TypeDefinitionEnum $
|
||||
G.EnumTypeDefinition
|
||||
{ G._etdDescription = dDescription,
|
||||
G._etdName = dName,
|
||||
G._etdDirectives = noDirectives,
|
||||
G._etdDirectives = dDirectives,
|
||||
G._etdValueDefinitions = map convertEnumValue $ NonEmpty.toList enumInfo
|
||||
}
|
||||
TIInputObject (InputObjectInfo values) ->
|
||||
@ -42,7 +41,7 @@ convertType (SomeDefinitionTypeInfo Definition {..}) = case dInfo of
|
||||
G.InputObjectTypeDefinition
|
||||
{ G._iotdDescription = dDescription,
|
||||
G._iotdName = dName,
|
||||
G._iotdDirectives = noDirectives,
|
||||
G._iotdDirectives = dDirectives,
|
||||
G._iotdValueDefinitions = map convertInputField values
|
||||
}
|
||||
TIObject (ObjectInfo fields interfaces) ->
|
||||
@ -50,7 +49,7 @@ convertType (SomeDefinitionTypeInfo Definition {..}) = case dInfo of
|
||||
G.ObjectTypeDefinition
|
||||
{ G._otdDescription = dDescription,
|
||||
G._otdName = dName,
|
||||
G._otdDirectives = noDirectives,
|
||||
G._otdDirectives = dDirectives,
|
||||
G._otdImplementsInterfaces = map getDefinitionName interfaces,
|
||||
G._otdFieldsDefinition = map convertField fields
|
||||
}
|
||||
@ -59,7 +58,7 @@ convertType (SomeDefinitionTypeInfo Definition {..}) = case dInfo of
|
||||
G.InterfaceTypeDefinition
|
||||
{ G._itdDescription = dDescription,
|
||||
G._itdName = dName,
|
||||
G._itdDirectives = noDirectives,
|
||||
G._itdDirectives = dDirectives,
|
||||
G._itdFieldsDefinition = map convertField fields,
|
||||
G._itdPossibleTypes = map getDefinitionName possibleTypes
|
||||
}
|
||||
@ -68,7 +67,7 @@ convertType (SomeDefinitionTypeInfo Definition {..}) = case dInfo of
|
||||
G.UnionTypeDefinition
|
||||
{ G._utdDescription = dDescription,
|
||||
G._utdName = dName,
|
||||
G._utdDirectives = noDirectives,
|
||||
G._utdDirectives = dDirectives,
|
||||
G._utdMemberTypes = map getDefinitionName possibleTypes
|
||||
}
|
||||
|
||||
@ -77,7 +76,7 @@ convertEnumValue Definition {..} =
|
||||
G.EnumValueDefinition
|
||||
{ G._evdDescription = dDescription,
|
||||
G._evdName = G.EnumValue dName,
|
||||
G._evdDirectives = noDirectives
|
||||
G._evdDirectives = dDirectives
|
||||
}
|
||||
|
||||
convertInputField :: Definition origin (InputFieldInfo origin) -> G.InputValueDefinition
|
||||
@ -88,7 +87,7 @@ convertInputField Definition {..} = case dInfo of
|
||||
G._ivdName = dName,
|
||||
G._ivdType = toGraphQLType typeInfo,
|
||||
G._ivdDefaultValue = defaultValue,
|
||||
G._ivdDirectives = noDirectives
|
||||
G._ivdDirectives = dDirectives
|
||||
}
|
||||
|
||||
convertField :: Definition origin (FieldInfo origin) -> G.FieldDefinition G.InputValueDefinition
|
||||
@ -99,13 +98,10 @@ convertField Definition {..} = case dInfo of
|
||||
G._fldName = dName,
|
||||
G._fldArgumentsDefinition = map convertInputField arguments,
|
||||
G._fldType = toGraphQLType typeInfo,
|
||||
G._fldDirectives = noDirectives
|
||||
G._fldDirectives = dDirectives
|
||||
}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
getDefinitionName :: Definition origin a -> G.Name
|
||||
getDefinitionName = dName
|
||||
|
||||
noDirectives :: [G.Directive Void]
|
||||
noDirectives = []
|
||||
|
@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-}
|
||||
|
||||
module Hasura.GraphQL.Schema
|
||||
( buildGQLContext,
|
||||
@ -17,6 +18,7 @@ import Data.Text.Extended
|
||||
import Data.Text.NonEmpty qualified as NT
|
||||
import Hasura.Base.Error
|
||||
import Hasura.Base.ErrorMessage (toErrorMessage)
|
||||
import Hasura.GraphQL.ApolloFederation
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Execute.Types
|
||||
import Hasura.GraphQL.Namespace
|
||||
@ -194,8 +196,9 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
HasuraSchema
|
||||
(remoteRelationshipField sources (fst <$> remotes) remoteSchemaPermsCtx)
|
||||
runMonadSchema schemaOptions schemaContext role $ do
|
||||
-- build all sources
|
||||
(sourcesQueryFields, sourcesMutationFrontendFields, sourcesMutationBackendFields, subscriptionFields) <-
|
||||
-- build all sources (`apolloFedTableParsers` contains all the parsers and
|
||||
-- type names, which are eligible for the `_Entity` Union)
|
||||
(sourcesQueryFields, sourcesMutationFrontendFields, sourcesMutationBackendFields, subscriptionFields, apolloFedTableParsers) <-
|
||||
fmap mconcat $ traverse (buildBackendSource buildSource) $ toList sources
|
||||
-- build all remote schemas
|
||||
-- we only keep the ones that don't result in a name conflict
|
||||
@ -204,6 +207,7 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
let remotesQueryFields = concatMap piQuery remoteSchemaFields
|
||||
remotesMutationFields = concat $ mapMaybe piMutation remoteSchemaFields
|
||||
remotesSubscriptionFields = concat $ mapMaybe piSubscription remoteSchemaFields
|
||||
apolloFields = apolloRootFields expFeatures apolloFedTableParsers
|
||||
|
||||
mutationParserFrontend <-
|
||||
buildMutationParser remotesMutationFields allActionInfos customTypes sourcesMutationFrontendFields
|
||||
@ -212,9 +216,9 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
subscriptionParser <-
|
||||
buildSubscriptionParser subscriptionFields allActionInfos customTypes remotesSubscriptionFields
|
||||
queryParserFrontend <-
|
||||
buildQueryParser sourcesQueryFields remotesQueryFields allActionInfos customTypes mutationParserFrontend subscriptionParser
|
||||
buildQueryParser sourcesQueryFields remotesQueryFields apolloFields allActionInfos customTypes mutationParserFrontend subscriptionParser
|
||||
queryParserBackend <-
|
||||
buildQueryParser sourcesQueryFields remotesQueryFields allActionInfos customTypes mutationParserBackend subscriptionParser
|
||||
buildQueryParser sourcesQueryFields remotesQueryFields apolloFields allActionInfos customTypes mutationParserBackend subscriptionParser
|
||||
|
||||
-- In order to catch errors early, we attempt to generate the data
|
||||
-- required for introspection, which ends up doing a few correctness
|
||||
@ -228,6 +232,7 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
(P.parserType <$> mutationParserBackend)
|
||||
(P.parserType <$> subscriptionParser)
|
||||
pure $
|
||||
-- We don't need to persist the introspection schema for all the roles here.
|
||||
-- TODO(nicuveo): we treat the admin role differently in this function,
|
||||
-- which is a bit inelegant; we might want to refactor this function and
|
||||
-- split it into several steps, so that we can make a separate function for
|
||||
@ -235,6 +240,7 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
if role == adminRoleName
|
||||
then result
|
||||
else G.SchemaIntrospection mempty
|
||||
|
||||
void $
|
||||
buildIntrospectionSchema
|
||||
(P.parserType queryParserFrontend)
|
||||
@ -268,16 +274,17 @@ buildRoleContext options sources remotes allActionInfos customTypes role remoteS
|
||||
( [FieldParser P.Parse (NamespacedField (QueryRootField UnpreparedValue))],
|
||||
[FieldParser P.Parse (NamespacedField (MutationRootField UnpreparedValue))],
|
||||
[FieldParser P.Parse (NamespacedField (MutationRootField UnpreparedValue))],
|
||||
[FieldParser P.Parse (NamespacedField (QueryRootField UnpreparedValue))]
|
||||
[FieldParser P.Parse (NamespacedField (QueryRootField UnpreparedValue))],
|
||||
[(G.Name, Parser 'Output P.Parse (ApolloFederationParserFunction P.Parse))]
|
||||
)
|
||||
buildSource sourceInfo@(SourceInfo _ tables functions _ _ sourceCustomization') =
|
||||
withSourceCustomization sourceCustomization (namingConventionSupport @b) globalDefaultNC do
|
||||
let validFunctions = takeValidFunctions functions
|
||||
validTables = takeValidTables tables
|
||||
makeTypename <- asks getter
|
||||
(uncustomizedQueryRootFields, uncustomizedSubscriptionRootFields) <-
|
||||
(uncustomizedQueryRootFields, uncustomizedSubscriptionRootFields, apolloFedTableParsers) <-
|
||||
buildQueryAndSubscriptionFields sourceInfo validTables validFunctions streamingSubscriptionsCtx
|
||||
(,,,)
|
||||
(,,,,apolloFedTableParsers)
|
||||
<$> customizeFields
|
||||
sourceCustomization
|
||||
(makeTypename <> MkTypename (<> Name.__query))
|
||||
@ -571,7 +578,7 @@ buildQueryAndSubscriptionFields ::
|
||||
TableCache b ->
|
||||
FunctionCache b ->
|
||||
StreamingSubscriptionsCtx ->
|
||||
m ([P.FieldParser n (QueryRootField UnpreparedValue)], [P.FieldParser n (SubscriptionRootField UnpreparedValue)])
|
||||
m ([P.FieldParser n (QueryRootField UnpreparedValue)], [P.FieldParser n (SubscriptionRootField UnpreparedValue)], [(G.Name, Parser 'Output n (ApolloFederationParserFunction n))])
|
||||
buildQueryAndSubscriptionFields sourceInfo tables (takeExposedAs FEAQuery -> functions) streamingSubsCtx = do
|
||||
roleName <- asks getter
|
||||
functionPermsCtx <- retrieve Options.soInferFunctionPermissions
|
||||
@ -585,8 +592,8 @@ buildQueryAndSubscriptionFields sourceInfo tables (takeExposedAs FEAQuery -> fun
|
||||
let targetTableName = _fiReturnType functionInfo
|
||||
lift $ mkRFs $ buildFunctionQueryFields sourceInfo functionName functionInfo targetTableName
|
||||
|
||||
(tableQueryFields, tableSubscriptionFields) <-
|
||||
unzip . catMaybes
|
||||
(tableQueryFields, tableSubscriptionFields, apolloFedTableParsers) <-
|
||||
unzip3 . catMaybes
|
||||
<$> for (Map.toList tables) \(tableName, tableInfo) -> runMaybeT $ do
|
||||
tableIdentifierName <- getTableIdentifierName @b tableInfo
|
||||
lift $ buildTableQueryAndSubscriptionFields sourceInfo tableName tableInfo streamingSubsCtx tableIdentifierName
|
||||
@ -596,7 +603,8 @@ buildQueryAndSubscriptionFields sourceInfo tables (takeExposedAs FEAQuery -> fun
|
||||
|
||||
pure
|
||||
( tableQueryRootFields <> functionSelectExpParsers,
|
||||
tableSubscriptionRootFields <> functionSelectExpParsers
|
||||
tableSubscriptionRootFields <> functionSelectExpParsers,
|
||||
catMaybes apolloFedTableParsers
|
||||
)
|
||||
where
|
||||
mkRFs = mkRootFields sourceName sourceConfig queryTagsConfig QDBR
|
||||
@ -689,14 +697,35 @@ buildQueryParser ::
|
||||
MonadBuildSchemaBase r m n =>
|
||||
[P.FieldParser n (NamespacedField (QueryRootField UnpreparedValue))] ->
|
||||
[P.FieldParser n (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))] ->
|
||||
[P.FieldParser n (G.SchemaIntrospection -> QueryRootField UnpreparedValue)] ->
|
||||
[ActionInfo] ->
|
||||
AnnotatedCustomTypes ->
|
||||
Maybe (Parser 'Output n (RootFieldMap (MutationRootField UnpreparedValue))) ->
|
||||
Maybe (Parser 'Output n (RootFieldMap (QueryRootField UnpreparedValue))) ->
|
||||
m (Parser 'Output n (RootFieldMap (QueryRootField UnpreparedValue)))
|
||||
buildQueryParser sourceQueryFields remoteQueryFields allActions customTypes mutationParser subscriptionParser = do
|
||||
buildQueryParser sourceQueryFields remoteQueryFields apolloFederationFields allActions customTypes mutationParser subscriptionParser = do
|
||||
actionQueryFields <- concat <$> traverse (buildActionQueryFields customTypes) allActions
|
||||
let allQueryFields = sourceQueryFields <> fmap (fmap NotNamespaced) actionQueryFields <> fmap (fmap $ fmap RFRemote) remoteQueryFields
|
||||
-- This method is aware of our rudimentary support for Apollo federation.
|
||||
-- Apollo federation adds two fields, `_service` and `_entities`. The
|
||||
-- `_service` field parser is a selection set that contains an `sdl` field.
|
||||
-- The `sdl` field, exposes a _serialized_ introspection of the schema. So in
|
||||
-- that sense it is similar to the `__type` and `__schema` introspection
|
||||
-- fields. However, a few things must be excluded from this introspection
|
||||
-- data, notably the Apollo federation fields `_service` and `_entities`
|
||||
-- themselves. So in this method we build a version of the introspection for
|
||||
-- Apollo federation purposes.
|
||||
let partialApolloQueryFP = sourceQueryFields <> fmap (fmap NotNamespaced) actionQueryFields <> fmap (fmap $ fmap RFRemote) remoteQueryFields
|
||||
basicQueryPForApollo <- queryRootFromFields partialApolloQueryFP
|
||||
let buildApolloIntrospection buildQRF = do
|
||||
partialSchema <-
|
||||
parseBuildIntrospectionSchema
|
||||
(P.parserType basicQueryPForApollo)
|
||||
(P.parserType <$> mutationParser)
|
||||
(P.parserType <$> subscriptionParser)
|
||||
pure $ NotNamespaced $ buildQRF $ convertToSchemaIntrospection partialSchema
|
||||
apolloFederationFieldsWithIntrospection :: [P.FieldParser n (NamespacedField (QueryRootField UnpreparedValue))]
|
||||
apolloFederationFieldsWithIntrospection = apolloFederationFields <&> (`P.bindField` buildApolloIntrospection)
|
||||
allQueryFields = partialApolloQueryFP <> apolloFederationFieldsWithIntrospection
|
||||
queryWithIntrospectionHelper allQueryFields mutationParser subscriptionParser
|
||||
|
||||
-- | Builds a @Schema@ at query parsing time
|
||||
|
@ -178,6 +178,8 @@ actionAsyncQuery objectTypes actionInfo = runMaybeT do
|
||||
actionOutputParser <- lift $ actionOutputFields outputType aot objectTypes
|
||||
let desc = G.Description $ "fields of action: " <>> actionName
|
||||
selectionSet =
|
||||
-- Note: If we want support for Apollo Federation for Actions later,
|
||||
-- we'd need to add support for "key" directive here as well.
|
||||
P.selectionSet outputTypeName (Just desc) (allFieldParsers actionOutputParser)
|
||||
<&> parsedSelectionsToFields IR.AsyncTypename
|
||||
pure $ P.subselection fieldName description actionIdInputField selectionSet
|
||||
@ -422,7 +424,7 @@ customScalarParser = \case
|
||||
| _stdName == GName._Boolean -> J.toJSON <$> P.boolean
|
||||
| otherwise -> P.jsonScalar _stdName _stdDescription
|
||||
ASTReusedScalar name backendScalarType ->
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing P.TIScalar
|
||||
let schemaType = P.TNamed P.NonNullable $ P.Definition name Nothing Nothing [] P.TIScalar
|
||||
backendScalarValidator =
|
||||
AB.dispatchAnyBackend @Backend backendScalarType \(scalarType :: ScalarWrapper b) jsonInput -> do
|
||||
-- We attempt to parse the value from JSON to validate it, but still
|
||||
@ -456,5 +458,6 @@ customEnumParser (EnumTypeDefinition typeName description enumValues) =
|
||||
valueName
|
||||
(_evdDescription enumValue)
|
||||
Nothing
|
||||
[]
|
||||
P.EnumValueInfo
|
||||
in P.enum enumName description enumValueDefinitions
|
||||
|
@ -39,6 +39,7 @@ where
|
||||
import Data.Has
|
||||
import Data.Text.Casing (GQLNameIdentifier)
|
||||
import Hasura.Base.Error
|
||||
import Hasura.GraphQL.ApolloFederation (ApolloFederationParserFunction)
|
||||
import Hasura.GraphQL.Schema.Common
|
||||
import Hasura.GraphQL.Schema.NamingCase
|
||||
import Hasura.GraphQL.Schema.Parser hiding (Type)
|
||||
@ -104,7 +105,8 @@ class
|
||||
GQLNameIdentifier ->
|
||||
m
|
||||
( [FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))],
|
||||
[FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))]
|
||||
[FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))],
|
||||
Maybe (G.Name, Parser 'Output n (ApolloFederationParserFunction n))
|
||||
)
|
||||
buildTableStreamingSubscriptionFields ::
|
||||
MonadBuildSchema b r m n =>
|
||||
|
@ -57,14 +57,17 @@ where
|
||||
import Data.Has (getter)
|
||||
import Data.Text.Casing qualified as C
|
||||
import Data.Text.Extended
|
||||
import Hasura.GraphQL.ApolloFederation
|
||||
import Hasura.GraphQL.Schema.Backend (BackendTableSelectSchema (..), MonadBuildSchema)
|
||||
import Hasura.GraphQL.Schema.Common
|
||||
import Hasura.GraphQL.Schema.Mutation
|
||||
import Hasura.GraphQL.Schema.NamingCase
|
||||
import Hasura.GraphQL.Schema.Options qualified as Options
|
||||
import Hasura.GraphQL.Schema.Parser hiding (EnumValueInfo, field)
|
||||
import Hasura.GraphQL.Schema.Select
|
||||
import Hasura.GraphQL.Schema.SubscriptionStream (selectStreamTable)
|
||||
import Hasura.GraphQL.Schema.Table (tableSelectPermissions)
|
||||
import Hasura.GraphQL.Schema.Table (getTableGQLName, tableSelectPermissions)
|
||||
import Hasura.GraphQL.Schema.Typename (mkTypename)
|
||||
import Hasura.GraphQL.Schema.Update (updateTable, updateTableByPk)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR
|
||||
@ -110,7 +113,8 @@ buildTableQueryAndSubscriptionFields ::
|
||||
C.GQLNameIdentifier ->
|
||||
m
|
||||
( [FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))],
|
||||
[FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))]
|
||||
[FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))],
|
||||
Maybe (G.Name, Parser 'Output n (ApolloFederationParserFunction n))
|
||||
)
|
||||
buildTableQueryAndSubscriptionFields sourceInfo tableName tableInfo streamSubCtx gqlName = do
|
||||
tCase <- asks getter
|
||||
@ -130,7 +134,7 @@ buildTableQueryAndSubscriptionFields sourceInfo tableName tableInfo streamSubCtx
|
||||
case selectPermission of
|
||||
-- No select permission found for the current role, so
|
||||
-- no root fields will be accessible to the role
|
||||
Nothing -> pure (mempty, mempty)
|
||||
Nothing -> pure (mempty, mempty, Nothing)
|
||||
-- Filter the root fields which have been enabled
|
||||
Just SelPermInfo {..} -> do
|
||||
selectStreamParser <-
|
||||
@ -161,7 +165,19 @@ buildTableQueryAndSubscriptionFields sourceInfo tableName tableInfo streamSubCtx
|
||||
selectStreamParser
|
||||
<> catMaybes [subscriptionSelectTableParser, subscriptionSelectTableByPkParser, subscriptionSelectTableAggParser]
|
||||
|
||||
pure (queryRootFields, subscriptionRootFields)
|
||||
-- This parser is for generating apollo federation field _entities
|
||||
apolloFedTableParser <- runMaybeT do
|
||||
guard $ isApolloFedV1enabled (_tciApolloFederationConfig (_tiCoreInfo tableInfo))
|
||||
tableSelSet <- MaybeT $ tableSelectionSet sourceInfo tableInfo
|
||||
selectPerm <- MaybeT $ tableSelectPermissions tableInfo
|
||||
stringifyNumbers <- retrieve Options.soStringifyNumbers
|
||||
primaryKeys <- hoistMaybe $ fmap _pkColumns . _tciPrimaryKey . _tiCoreInfo $ tableInfo
|
||||
let tableSelPerm = tablePermissionsInfo selectPerm
|
||||
tableGQLName <- getTableGQLName tableInfo
|
||||
objectTypename <- mkTypename tableGQLName
|
||||
pure $ (objectTypename, convertToApolloFedParserFunc sourceInfo tableInfo tableSelPerm stringifyNumbers (Just tCase) primaryKeys tableSelSet)
|
||||
|
||||
pure (queryRootFields, subscriptionRootFields, apolloFedTableParser)
|
||||
where
|
||||
selectDesc = buildFieldDescription defaultSelectDesc $ _crfComment _tcrfSelect
|
||||
selectPKDesc = buildFieldDescription defaultSelectPKDesc $ _crfComment _tcrfSelectByPk
|
||||
|
@ -334,17 +334,17 @@ typeField =
|
||||
J.String "NON_NULL"
|
||||
P.TList P.Nullable _ ->
|
||||
J.String "LIST"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ P.TIScalar) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ P.TIScalar) ->
|
||||
J.String "SCALAR"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIEnum _)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIEnum _)) ->
|
||||
J.String "ENUM"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIInputObject _)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIInputObject _)) ->
|
||||
J.String "INPUT_OBJECT"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIObject _)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIObject _)) ->
|
||||
J.String "OBJECT"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIInterface _)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIInterface _)) ->
|
||||
J.String "INTERFACE"
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIUnion _)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIUnion _)) ->
|
||||
J.String "UNION"
|
||||
name :: FieldParser n (SomeType -> J.Value)
|
||||
name =
|
||||
@ -352,14 +352,14 @@ typeField =
|
||||
$> \case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition name' _ _ _) ->
|
||||
P.TNamed P.Nullable (P.Definition name' _ _ _ _) ->
|
||||
nameAsJSON name'
|
||||
_ -> J.Null
|
||||
description :: FieldParser n (SomeType -> J.Value)
|
||||
description =
|
||||
P.selection_ GName._description Nothing P.string
|
||||
$> \case
|
||||
SomeType (P.TNamed _ (P.Definition _ (Just desc) _ _)) ->
|
||||
SomeType (P.TNamed _ (P.Definition _ (Just desc) _ _ _)) ->
|
||||
J.String (G.unDescription desc)
|
||||
_ -> J.Null
|
||||
fields :: FieldParser n (SomeType -> J.Value)
|
||||
@ -370,9 +370,9 @@ typeField =
|
||||
\case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIObject (P.ObjectInfo fields' _interfaces'))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIObject (P.ObjectInfo fields' _interfaces'))) ->
|
||||
J.Array $ V.fromList $ printer <$> fields'
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIInterface (P.InterfaceInfo fields' _objects'))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIInterface (P.InterfaceInfo fields' _objects'))) ->
|
||||
J.Array $ V.fromList $ printer <$> fields'
|
||||
_ -> J.Null
|
||||
interfaces :: FieldParser n (SomeType -> J.Value)
|
||||
@ -382,7 +382,7 @@ typeField =
|
||||
\case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIObject (P.ObjectInfo _fields' interfaces'))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIObject (P.ObjectInfo _fields' interfaces'))) ->
|
||||
J.Array $ V.fromList $ printer . SomeType . P.TNamed P.Nullable . fmap P.TIInterface <$> interfaces'
|
||||
_ -> J.Null
|
||||
possibleTypes :: FieldParser n (SomeType -> J.Value)
|
||||
@ -392,9 +392,9 @@ typeField =
|
||||
\case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIInterface (P.InterfaceInfo _fields' objects'))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIInterface (P.InterfaceInfo _fields' objects'))) ->
|
||||
J.Array $ V.fromList $ printer . SomeType . P.TNamed P.Nullable . fmap P.TIObject <$> objects'
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIUnion (P.UnionInfo objects'))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIUnion (P.UnionInfo objects'))) ->
|
||||
J.Array $ V.fromList $ printer . SomeType . P.TNamed P.Nullable . fmap P.TIObject <$> objects'
|
||||
_ -> J.Null
|
||||
enumValues :: FieldParser n (SomeType -> J.Value)
|
||||
@ -405,7 +405,7 @@ typeField =
|
||||
\case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIEnum vals)) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIEnum vals)) ->
|
||||
J.Array $ V.fromList $ fmap printer $ toList vals
|
||||
_ -> J.Null
|
||||
inputFields :: FieldParser n (SomeType -> J.Value)
|
||||
@ -415,7 +415,7 @@ typeField =
|
||||
\case
|
||||
SomeType tp ->
|
||||
case tp of
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ (P.TIInputObject (P.InputObjectInfo fieldDefs))) ->
|
||||
P.TNamed P.Nullable (P.Definition _ _ _ _ (P.TIInputObject (P.InputObjectInfo fieldDefs))) ->
|
||||
J.Array $ V.fromList $ map printer fieldDefs
|
||||
_ -> J.Null
|
||||
-- ofType peels modalities off of types
|
||||
@ -562,7 +562,7 @@ typeKind =
|
||||
]
|
||||
)
|
||||
where
|
||||
mkDefinition name = (P.Definition name Nothing Nothing P.EnumValueInfo, ())
|
||||
mkDefinition name = (P.Definition name Nothing Nothing [] P.EnumValueInfo, ())
|
||||
|
||||
{-
|
||||
type __Field {
|
||||
|
@ -376,7 +376,7 @@ remoteFieldScalarParser customizeTypename (G.ScalarTypeDefinition description na
|
||||
}
|
||||
where
|
||||
customizedTypename = runMkTypename customizeTypename name
|
||||
schemaType = TNamed NonNullable $ Definition customizedTypename description Nothing TIScalar
|
||||
schemaType = TNamed NonNullable $ Definition customizedTypename description Nothing [] TIScalar
|
||||
gType = toGraphQLType schemaType
|
||||
|
||||
mkRemoteGType = \case
|
||||
@ -391,7 +391,7 @@ remoteFieldEnumParser ::
|
||||
remoteFieldEnumParser customizeTypename (G.EnumTypeDefinition desc name _directives valueDefns) =
|
||||
let enumValDefns =
|
||||
valueDefns <&> \(G.EnumValueDefinition enumDesc enumName _) ->
|
||||
( Definition (G.unEnumValue enumName) enumDesc Nothing P.EnumValueInfo,
|
||||
( Definition (G.unEnumValue enumName) enumDesc Nothing [] P.EnumValueInfo,
|
||||
G.VEnum enumName
|
||||
)
|
||||
in fmap (Altered False,) $ P.enum (runMkTypename customizeTypename name) desc $ NE.fromList enumValDefns
|
||||
@ -839,12 +839,12 @@ remoteFieldFromDefinition schemaDoc parentTypeName remoteRelationships (G.FieldD
|
||||
convertType gType
|
||||
where
|
||||
addNullableList :: FieldParser n a -> FieldParser n a
|
||||
addNullableList (P.FieldParser (Definition name' desc origin (FieldInfo args typ)) parser) =
|
||||
P.FieldParser (Definition name' desc origin (FieldInfo args (TList Nullable typ))) parser
|
||||
addNullableList (P.FieldParser (Definition name' desc origin dLst (FieldInfo args typ)) parser) =
|
||||
P.FieldParser (Definition name' desc origin dLst (FieldInfo args (TList Nullable typ))) parser
|
||||
|
||||
addNonNullableList :: FieldParser n a -> FieldParser n a
|
||||
addNonNullableList (P.FieldParser (Definition name' desc origin (FieldInfo args typ)) parser) =
|
||||
P.FieldParser (Definition name' desc origin (FieldInfo args (TList NonNullable typ))) parser
|
||||
addNonNullableList (P.FieldParser (Definition name' desc origin dLst (FieldInfo args typ)) parser) =
|
||||
P.FieldParser (Definition name' desc origin dLst (FieldInfo args (TList NonNullable typ))) parser
|
||||
|
||||
-- TODO add directives, deprecation
|
||||
convertType ::
|
||||
|
@ -35,6 +35,7 @@ import Data.Has
|
||||
import Data.HashMap.Strict.Extended qualified as Map
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty qualified as NE
|
||||
import Data.Text qualified as T
|
||||
import Data.Text.Extended
|
||||
import Hasura.Backends.Postgres.SQL.Types qualified as PG
|
||||
import Hasura.Base.Error
|
||||
@ -383,6 +384,19 @@ defaultTableSelectionSet sourceInfo tableInfo = runMaybeT do
|
||||
let xRelay = relayExtension @b
|
||||
tableFields = Map.elems $ _tciFieldInfoMap tableCoreInfo
|
||||
tablePkeyColumns = _pkColumns <$> _tciPrimaryKey tableCoreInfo
|
||||
pkFields = concatMap toList tablePkeyColumns
|
||||
pkFieldDirective = T.intercalate " " $ map (G.unName . ciName) pkFields
|
||||
-- Adding `@key` directives to type for apollo federation. An example
|
||||
-- of type with key directive:
|
||||
-- type Product @key(fields: "upc sku"){
|
||||
-- upc: UPC!
|
||||
-- sku: SKU!
|
||||
-- name: String
|
||||
-- }
|
||||
pkDirectives =
|
||||
if isApolloFedV1enabled (_tciApolloFederationConfig tableCoreInfo) && (not . null) pkFields
|
||||
then [(G.Directive Name._key . Map.singleton Name._fields . G.VString) pkFieldDirective]
|
||||
else mempty
|
||||
description = G.Description . PG.getPGDescription <$> _tciDescription tableCoreInfo
|
||||
fieldParsers <-
|
||||
concat
|
||||
@ -406,16 +420,19 @@ defaultTableSelectionSet sourceInfo tableInfo = runMaybeT do
|
||||
allFieldParsers = fieldParsers <> [nodeIdFieldParser]
|
||||
nodeInterface <- runNodeBuilder nodeBuilder
|
||||
pure $
|
||||
P.selectionSetObject objectTypename description allFieldParsers [nodeInterface]
|
||||
selectionSetObjectWithDirective objectTypename description allFieldParsers [nodeInterface] pkDirectives
|
||||
<&> parsedSelectionsToFields IR.AFExpression
|
||||
_ ->
|
||||
pure $
|
||||
P.selectionSetObject objectTypename description fieldParsers []
|
||||
selectionSetObjectWithDirective objectTypename description fieldParsers [] pkDirectives
|
||||
<&> parsedSelectionsToFields IR.AFExpression
|
||||
where
|
||||
sourceName = _siName sourceInfo
|
||||
tableName = tableInfoName tableInfo
|
||||
tableCoreInfo = _tiCoreInfo tableInfo
|
||||
selectionSetObjectWithDirective name description parsers implementsInterfaces directives =
|
||||
P.setParserDirectives directives $
|
||||
P.selectionSetObject name description parsers implementsInterfaces
|
||||
|
||||
-- | List of table fields object.
|
||||
-- Just a @'nonNullableObjectList' wrapper over @'tableSelectionSet'.
|
||||
|
@ -78,7 +78,7 @@ cursorOrderingArgParser = do
|
||||
where
|
||||
define (name, val) =
|
||||
let orderingTypeDesc = bool "descending" "ascending" $ val == COAscending
|
||||
in P.Definition name (Just $ G.Description $ orderingTypeDesc <> " ordering of the cursor") Nothing P.EnumValueInfo
|
||||
in P.Definition name (Just $ G.Description $ orderingTypeDesc <> " ordering of the cursor") Nothing [] P.EnumValueInfo
|
||||
|
||||
-- | Argument to specify the ordering of the cursor.
|
||||
-- > ordering: cursor_ordering
|
||||
|
@ -106,7 +106,7 @@ tableSelectColumnsEnum sourceInfo tableInfo = do
|
||||
]
|
||||
where
|
||||
define name =
|
||||
P.Definition name (Just $ G.Description "column name") Nothing P.EnumValueInfo
|
||||
P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
||||
|
||||
-- | Table update columns enum
|
||||
--
|
||||
@ -129,7 +129,7 @@ tableUpdateColumnsEnum tableInfo = do
|
||||
pure (define $ ciName column, ciColumn column)
|
||||
pure $ P.enum enumName enumDesc <$> nonEmpty enumValues
|
||||
where
|
||||
define name = P.Definition name (Just $ G.Description "column name") Nothing P.EnumValueInfo
|
||||
define name = P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
||||
|
||||
-- If there's no column for which the current user has "update"
|
||||
-- permissions, this functions returns an enum that only contains a
|
||||
@ -148,7 +148,7 @@ updateColumnsPlaceholderParser tableInfo = do
|
||||
pure $
|
||||
P.enum enumName (Just $ G.Description $ "placeholder for update columns of table " <> tableInfoName tableInfo <<> " (current role has no relevant permissions)") $
|
||||
pure
|
||||
( P.Definition @_ @P.EnumValueInfo Name.__PLACEHOLDER (Just $ G.Description "placeholder (do not use)") Nothing P.EnumValueInfo,
|
||||
( P.Definition @_ @P.EnumValueInfo Name.__PLACEHOLDER (Just $ G.Description "placeholder (do not use)") Nothing [] P.EnumValueInfo,
|
||||
Nothing
|
||||
)
|
||||
|
||||
|
@ -264,6 +264,7 @@ filterVariablesFromQuery = foldMap \case
|
||||
RFRemote remote -> foldOf (traverse . _SessionPresetVariable . to match) remote
|
||||
RFAction actionQ -> foldMap remoteFieldPred actionQ
|
||||
RFRaw {} -> mempty
|
||||
RFMulti {} -> mempty
|
||||
where
|
||||
_SessionPresetVariable :: Traversal' RemoteSchemaVariable SessionVariable
|
||||
_SessionPresetVariable f (SessionPresetVariable a b c) =
|
||||
@ -484,6 +485,10 @@ runGQ env logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
-- For `ExecStepMulti`, execute all steps and then concat them in a list
|
||||
E.ExecStepMulti lst -> do
|
||||
_all <- traverse (executeQueryStep httpManager fieldName) lst
|
||||
pure $ AnnotatedResponsePart 0 Telem.Local (encJFromList (map arpResponse _all)) []
|
||||
|
||||
executeMutationStep ::
|
||||
HTTP.Manager ->
|
||||
@ -514,6 +519,10 @@ runGQ env logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
-- For `ExecStepMulti`, execute all steps and then concat them in a list
|
||||
E.ExecStepMulti lst -> do
|
||||
_all <- traverse (executeQueryStep httpManager fieldName) lst
|
||||
pure $ AnnotatedResponsePart 0 Telem.Local (encJFromList (map arpResponse _all)) []
|
||||
|
||||
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq remoteJoins = do
|
||||
(telemTimeIO_DT, remoteResponseHeaders, resp) <-
|
||||
|
@ -498,38 +498,43 @@ onStart env enabledLogTypes serverEnv wsConn (StartMsg opId q) onMessageActions
|
||||
Nothing -> do
|
||||
conclusion <- runExceptT $
|
||||
runLimits $
|
||||
forWithKey queryPlan $ \fieldName -> \case
|
||||
E.ExecStepDB _headers exists remoteJoins -> doQErr $ do
|
||||
(telemTimeIO_DT, resp) <-
|
||||
AB.dispatchAnyBackend @BackendTransport
|
||||
exists
|
||||
\(EB.DBStepInfo _ sourceConfig genSql tx :: EB.DBStepInfo b) ->
|
||||
runDBQuery @b
|
||||
requestId
|
||||
q
|
||||
fieldName
|
||||
userInfo
|
||||
logger
|
||||
sourceConfig
|
||||
tx
|
||||
genSql
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
||||
(time, (resp, _)) <- doQErr $ do
|
||||
(time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure (time, (finalResponse, hdrs))
|
||||
pure $ AnnotatedResponsePart time Telem.Empty resp []
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
forWithKey queryPlan $ \fieldName ->
|
||||
let getResponse = \case
|
||||
E.ExecStepDB _headers exists remoteJoins -> doQErr $ do
|
||||
(telemTimeIO_DT, resp) <-
|
||||
AB.dispatchAnyBackend @BackendTransport
|
||||
exists
|
||||
\(EB.DBStepInfo _ sourceConfig genSql tx :: EB.DBStepInfo b) ->
|
||||
runDBQuery @b
|
||||
requestId
|
||||
q
|
||||
fieldName
|
||||
userInfo
|
||||
logger
|
||||
sourceConfig
|
||||
tx
|
||||
genSql
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
||||
(time, (resp, _)) <- doQErr $ do
|
||||
(time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure (time, (finalResponse, hdrs))
|
||||
pure $ AnnotatedResponsePart time Telem.Empty resp []
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
E.ExecStepMulti lst -> do
|
||||
allResponses <- traverse getResponse lst
|
||||
pure $ AnnotatedResponsePart 0 Telem.Local (encJFromList (map arpResponse allResponses)) []
|
||||
in getResponse
|
||||
sendResultFromFragments Telem.Query timerTot requestId conclusion opName parameterizedQueryHash gqlOpType
|
||||
case conclusion of
|
||||
Left _ -> pure ()
|
||||
@ -568,39 +573,44 @@ onStart env enabledLogTypes serverEnv wsConn (StartMsg opId q) onMessageActions
|
||||
Nothing -> do
|
||||
conclusion <- runExceptT $
|
||||
runLimits $
|
||||
forWithKey mutationPlan $ \fieldName -> \case
|
||||
-- Ignoring response headers since we can't send them over WebSocket
|
||||
E.ExecStepDB _responseHeaders exists remoteJoins -> doQErr $ do
|
||||
(telemTimeIO_DT, resp) <-
|
||||
AB.dispatchAnyBackend @BackendTransport
|
||||
exists
|
||||
\(EB.DBStepInfo _ sourceConfig genSql tx :: EB.DBStepInfo b) ->
|
||||
runDBMutation @b
|
||||
requestId
|
||||
q
|
||||
fieldName
|
||||
userInfo
|
||||
logger
|
||||
sourceConfig
|
||||
tx
|
||||
genSql
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
||||
(time, (resp, hdrs)) <- doQErr $ do
|
||||
(time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure (time, (finalResponse, hdrs))
|
||||
pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs
|
||||
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
forWithKey mutationPlan $ \fieldName ->
|
||||
let getResponse = \case
|
||||
-- Ignoring response headers since we can't send them over WebSocket
|
||||
E.ExecStepDB _responseHeaders exists remoteJoins -> doQErr $ do
|
||||
(telemTimeIO_DT, resp) <-
|
||||
AB.dispatchAnyBackend @BackendTransport
|
||||
exists
|
||||
\(EB.DBStepInfo _ sourceConfig genSql tx :: EB.DBStepInfo b) ->
|
||||
runDBMutation @b
|
||||
requestId
|
||||
q
|
||||
fieldName
|
||||
userInfo
|
||||
logger
|
||||
sourceConfig
|
||||
tx
|
||||
genSql
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
||||
(time, (resp, hdrs)) <- doQErr $ do
|
||||
(time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan
|
||||
finalResponse <-
|
||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||
pure (time, (finalResponse, hdrs))
|
||||
pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs
|
||||
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||
E.ExecStepRaw json -> do
|
||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
||||
buildRaw json
|
||||
E.ExecStepMulti lst -> do
|
||||
allResponses <- traverse getResponse lst
|
||||
pure $ AnnotatedResponsePart 0 Telem.Local (encJFromList (map arpResponse allResponses)) []
|
||||
in getResponse
|
||||
sendResultFromFragments Telem.Query timerTot requestId conclusion opName parameterizedQueryHash gqlOpType
|
||||
liftIO $ sendCompleted (Just requestId) (Just parameterizedQueryHash)
|
||||
E.SubscriptionExecutionPlan subExec -> do
|
||||
|
@ -577,3 +577,32 @@ _Node = [G.name|Node|]
|
||||
|
||||
___hasura_internal_typename :: G.Name
|
||||
___hasura_internal_typename = [G.name|__hasura_internal_typename|]
|
||||
|
||||
-- * Apollo Federation
|
||||
|
||||
__service :: G.Name
|
||||
__service = [G.name|_service|]
|
||||
|
||||
_key :: G.Name
|
||||
_key = [G.name|key|]
|
||||
|
||||
_fields :: G.Name
|
||||
_fields = [G.name|fields|]
|
||||
|
||||
_representations :: G.Name
|
||||
_representations = [G.name|representations|]
|
||||
|
||||
__Any :: G.Name
|
||||
__Any = [G.name|_Any|]
|
||||
|
||||
_sdl :: G.Name
|
||||
_sdl = [G.name|sdl|]
|
||||
|
||||
__Service :: G.Name
|
||||
__Service = [G.name|_Service|]
|
||||
|
||||
__Entity :: G.Name
|
||||
__Entity = [G.name|_Entity|]
|
||||
|
||||
__entities :: G.Name
|
||||
__entities = [G.name|_entities|]
|
||||
|
@ -5,7 +5,9 @@
|
||||
-- | Types/functions shared between modules that implement "Hasura.RQL.DDL.Schema.Cache". Other
|
||||
-- modules should not import this module directly.
|
||||
module Hasura.RQL.DDL.Schema.Cache.Common
|
||||
( BuildOutputs (..),
|
||||
( ApolloFederationConfig (..),
|
||||
ApolloFederationVersion (..),
|
||||
BuildOutputs (..),
|
||||
CacheBuild,
|
||||
CacheBuildParams (CacheBuildParams),
|
||||
InvalidationKeys (..),
|
||||
@ -112,9 +114,10 @@ invalidateKeys CacheInvalidations {..} InvalidationKeys {..} =
|
||||
invalidate = M.alter $ Just . maybe Inc.initialInvalidationKey Inc.invalidate
|
||||
|
||||
data TableBuildInput b = TableBuildInput
|
||||
{ _tbiName :: !(TableName b),
|
||||
_tbiIsEnum :: !Bool,
|
||||
_tbiConfiguration :: !(TableConfig b)
|
||||
{ _tbiName :: TableName b,
|
||||
_tbiIsEnum :: Bool,
|
||||
_tbiConfiguration :: TableConfig b,
|
||||
_tbiApolloFederationConfig :: Maybe ApolloFederationConfig
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
@ -151,7 +154,7 @@ mkTableInputs ::
|
||||
mkTableInputs TableMetadata {..} =
|
||||
(buildInput, nonColumns, permissions)
|
||||
where
|
||||
buildInput = TableBuildInput _tmTable _tmIsEnum _tmConfiguration
|
||||
buildInput = TableBuildInput _tmTable _tmIsEnum _tmConfiguration _tmApolloFederationConfig
|
||||
nonColumns =
|
||||
NonColumnTableInputs
|
||||
_tmTable
|
||||
|
@ -64,7 +64,8 @@ import Language.GraphQL.Draft.Syntax qualified as G
|
||||
data TrackTable b = TrackTable
|
||||
{ tSource :: !SourceName,
|
||||
tName :: !(TableName b),
|
||||
tIsEnum :: !Bool
|
||||
tIsEnum :: !Bool,
|
||||
tApolloFedConfig :: !(Maybe ApolloFederationConfig)
|
||||
}
|
||||
|
||||
deriving instance (Backend b) => Show (TrackTable b)
|
||||
@ -79,7 +80,8 @@ instance (Backend b) => FromJSON (TrackTable b) where
|
||||
<$> o .:? "source" .!= defaultSource
|
||||
<*> o .: "table"
|
||||
<*> o .:? "is_enum" .!= False
|
||||
withoutOptions = TrackTable defaultSource <$> parseJSON v <*> pure False
|
||||
<*> o .:? "apollo_federation_config"
|
||||
withoutOptions = TrackTable defaultSource <$> parseJSON v <*> pure False <*> pure Nothing
|
||||
|
||||
data SetTableIsEnum = SetTableIsEnum
|
||||
{ stieSource :: !SourceName,
|
||||
@ -211,8 +213,9 @@ trackExistingTableOrViewP2 ::
|
||||
TableName b ->
|
||||
Bool ->
|
||||
TableConfig b ->
|
||||
Maybe ApolloFederationConfig ->
|
||||
m EncJSON
|
||||
trackExistingTableOrViewP2 source tableName isEnum config = do
|
||||
trackExistingTableOrViewP2 source tableName isEnum config apolloFedConfig = do
|
||||
sc <- askSchemaCache
|
||||
{-
|
||||
The next line does more than what it says on the tin. Removing the following
|
||||
@ -223,7 +226,7 @@ trackExistingTableOrViewP2 source tableName isEnum config = do
|
||||
memory usage happens even when no substantial GraphQL schema is generated.
|
||||
-}
|
||||
checkConflictingNode sc $ snakeCaseTableName @b tableName
|
||||
let metadata = mkTableMeta tableName isEnum config
|
||||
let metadata = tmApolloFederationConfig .~ apolloFedConfig $ mkTableMeta tableName isEnum config
|
||||
buildSchemaCacheFor
|
||||
( MOSourceObjId source $
|
||||
AB.mkAnyBackend $
|
||||
@ -238,9 +241,9 @@ runTrackTableQ ::
|
||||
(MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b) =>
|
||||
TrackTable b ->
|
||||
m EncJSON
|
||||
runTrackTableQ (TrackTable source qt isEnum) = do
|
||||
runTrackTableQ (TrackTable source qt isEnum apolloFedConfig) = do
|
||||
trackExistingTableOrViewP1 @b source qt
|
||||
trackExistingTableOrViewP2 @b source qt isEnum emptyTableConfig
|
||||
trackExistingTableOrViewP2 @b source qt isEnum emptyTableConfig apolloFedConfig
|
||||
|
||||
data TrackTableV2 b = TrackTableV2
|
||||
{ ttv2Table :: !(TrackTable b),
|
||||
@ -259,9 +262,9 @@ runTrackTableV2Q ::
|
||||
(MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b) =>
|
||||
TrackTableV2 b ->
|
||||
m EncJSON
|
||||
runTrackTableV2Q (TrackTableV2 (TrackTable source qt isEnum) config) = do
|
||||
runTrackTableV2Q (TrackTableV2 (TrackTable source qt isEnum apolloFedConfig) config) = do
|
||||
trackExistingTableOrViewP1 @b source qt
|
||||
trackExistingTableOrViewP2 @b source qt isEnum config
|
||||
trackExistingTableOrViewP2 @b source qt isEnum config apolloFedConfig
|
||||
|
||||
runSetExistingTableIsEnumQ :: (MonadError QErr m, CacheRWM m, MetadataM m) => SetTableIsEnum -> m EncJSON
|
||||
runSetExistingTableIsEnumQ (SetTableIsEnum source tableName isEnum) = do
|
||||
@ -468,7 +471,7 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
|
||||
)
|
||||
(TableCoreInfoG b (RawColumnInfo b) (Column b))
|
||||
buildRawTableInfo = Inc.cache proc (tableBuildInput, maybeInfo, sourceConfig, reloadMetadataInvalidationKey) -> do
|
||||
let TableBuildInput name isEnum config = tableBuildInput
|
||||
let TableBuildInput name isEnum config apolloFedConfig = tableBuildInput
|
||||
metadataTable <-
|
||||
(|
|
||||
onNothingA
|
||||
@ -505,7 +508,8 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
|
||||
_tciEnumValues = enumValues,
|
||||
_tciCustomConfig = config,
|
||||
_tciDescription = description,
|
||||
_tciExtraTableMetadata = _ptmiExtraTableMetadata metadataTable
|
||||
_tciExtraTableMetadata = _ptmiExtraTableMetadata metadataTable,
|
||||
_tciApolloFederationConfig = apolloFedConfig
|
||||
}
|
||||
|
||||
-- Step 2: Process the raw table cache to replace Postgres column types with logical column
|
||||
|
@ -42,6 +42,7 @@ data RootField (db :: BackendType -> Type) remote action raw where
|
||||
RFRemote :: remote -> RootField db remote action raw
|
||||
RFAction :: action -> RootField db remote action raw
|
||||
RFRaw :: raw -> RootField db remote action raw
|
||||
RFMulti :: [RootField db remote action raw] -> RootField db remote action raw
|
||||
|
||||
data MutationDB (b :: BackendType) (r :: Type) v
|
||||
= MDBInsert (AnnotatedInsert b r v)
|
||||
|
@ -45,10 +45,14 @@ module Hasura.RQL.Types.Common
|
||||
commentToMaybeText,
|
||||
commentFromMaybeText,
|
||||
EnvRecord (..),
|
||||
ApolloFederationConfig (..),
|
||||
ApolloFederationVersion (..),
|
||||
isApolloFedV1enabled,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.TH
|
||||
import Data.Aeson.Types (prependFailure, typeMismatch)
|
||||
@ -544,3 +548,34 @@ instance (ToJSON a) => ToJSON (EnvRecord a) where
|
||||
toJSON (EnvRecord envVar _envValue) = object ["env_var" .= envVar]
|
||||
|
||||
instance (FromJSON a) => FromJSON (EnvRecord a)
|
||||
|
||||
data ApolloFederationVersion = V1 deriving (Show, Eq, Generic)
|
||||
|
||||
instance Cacheable ApolloFederationVersion
|
||||
|
||||
instance ToJSON ApolloFederationVersion where
|
||||
toJSON V1 = J.String "v1"
|
||||
|
||||
instance FromJSON ApolloFederationVersion where
|
||||
parseJSON = withText "ApolloFederationVersion" $
|
||||
\case
|
||||
"v1" -> pure V1
|
||||
_ -> fail "enable takes the version of apollo federation. Supported value is v1 only."
|
||||
|
||||
instance NFData ApolloFederationVersion
|
||||
|
||||
data ApolloFederationConfig = ApolloFederationConfig
|
||||
{ enable :: !ApolloFederationVersion
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Cacheable ApolloFederationConfig
|
||||
|
||||
instance ToJSON ApolloFederationConfig
|
||||
|
||||
instance FromJSON ApolloFederationConfig
|
||||
|
||||
instance NFData ApolloFederationConfig
|
||||
|
||||
isApolloFedV1enabled :: Maybe ApolloFederationConfig -> Bool
|
||||
isApolloFedV1enabled = isJust
|
||||
|
@ -91,6 +91,7 @@ module Hasura.RQL.Types.Metadata
|
||||
tmComputedFields,
|
||||
tmConfiguration,
|
||||
tmDeletePermissions,
|
||||
tmApolloFederationConfig,
|
||||
tmEventTriggers,
|
||||
tmInsertPermissions,
|
||||
tmIsEnum,
|
||||
@ -323,7 +324,8 @@ data TableMetadata b = TableMetadata
|
||||
_tmSelectPermissions :: !(Permissions (SelPermDef b)),
|
||||
_tmUpdatePermissions :: !(Permissions (UpdPermDef b)),
|
||||
_tmDeletePermissions :: !(Permissions (DelPermDef b)),
|
||||
_tmEventTriggers :: !(EventTriggers b)
|
||||
_tmEventTriggers :: !(EventTriggers b),
|
||||
_tmApolloFederationConfig :: !(Maybe ApolloFederationConfig)
|
||||
}
|
||||
deriving (Generic)
|
||||
|
||||
@ -353,6 +355,7 @@ mkTableMeta qt isEnum config =
|
||||
mempty
|
||||
mempty
|
||||
mempty
|
||||
Nothing
|
||||
|
||||
instance (Backend b) => FromJSON (TableMetadata b) where
|
||||
parseJSON = withObject "Object" $ \o -> do
|
||||
@ -375,6 +378,7 @@ instance (Backend b) => FromJSON (TableMetadata b) where
|
||||
<*> parseListAsMap "update permissions" _pdRole (o .:? upKey .!= [])
|
||||
<*> parseListAsMap "delete permissions" _pdRole (o .:? dpKey .!= [])
|
||||
<*> parseListAsMap "event triggers" etcName (o .:? etKey .!= [])
|
||||
<*> o .:? enableAFKey
|
||||
where
|
||||
tableKey = "table"
|
||||
isEnumKey = "is_enum"
|
||||
@ -388,6 +392,7 @@ instance (Backend b) => FromJSON (TableMetadata b) where
|
||||
etKey = "event_triggers"
|
||||
cfKey = "computed_fields"
|
||||
rrKey = "remote_relationships"
|
||||
enableAFKey = "apollo_federation_config"
|
||||
|
||||
getUnexpectedKeys o =
|
||||
HS.fromList (KM.keys o) `HS.difference` expectedKeySet
|
||||
@ -405,7 +410,8 @@ instance (Backend b) => FromJSON (TableMetadata b) where
|
||||
dpKey,
|
||||
etKey,
|
||||
cfKey,
|
||||
rrKey
|
||||
rrKey,
|
||||
enableAFKey
|
||||
]
|
||||
|
||||
data FunctionMetadata b = FunctionMetadata
|
||||
@ -932,6 +938,7 @@ metadataToOrdJSON
|
||||
updatePermissions
|
||||
deletePermissions
|
||||
eventTriggers
|
||||
enableApolloFed
|
||||
) =
|
||||
AO.object $
|
||||
[("table", AO.toOrdered table)]
|
||||
@ -946,10 +953,12 @@ metadataToOrdJSON
|
||||
selectPermissionsPair,
|
||||
updatePermissionsPair,
|
||||
deletePermissionsPair,
|
||||
eventTriggersPair
|
||||
eventTriggersPair,
|
||||
apolloFedConfigPair
|
||||
]
|
||||
where
|
||||
isEnumPair = if isEnum then Just ("is_enum", AO.toOrdered isEnum) else Nothing
|
||||
apolloFedConfigPair = fmap (\afConfig -> ("apollo_federation_config", AO.toOrdered afConfig)) enableApolloFed
|
||||
configPair =
|
||||
if config == emptyTableConfig
|
||||
then Nothing
|
||||
|
@ -66,6 +66,7 @@ module Hasura.RQL.Types.Table
|
||||
tcColumnConfig,
|
||||
tciCustomConfig,
|
||||
tciDescription,
|
||||
tciApolloFederationConfig,
|
||||
tciEnumValues,
|
||||
tciExtraTableMetadata,
|
||||
tciFieldInfoMap,
|
||||
@ -867,7 +868,8 @@ data TableCoreInfoG (b :: BackendType) field primaryKeyColumn = TableCoreInfo
|
||||
_tciViewInfo :: Maybe ViewInfo,
|
||||
_tciEnumValues :: Maybe EnumValues,
|
||||
_tciCustomConfig :: TableConfig b,
|
||||
_tciExtraTableMetadata :: ExtraTableMetadata b
|
||||
_tciExtraTableMetadata :: ExtraTableMetadata b,
|
||||
_tciApolloFederationConfig :: Maybe ApolloFederationConfig
|
||||
}
|
||||
deriving (Generic)
|
||||
|
||||
|
@ -798,11 +798,12 @@ parseExperimentalFeatures =
|
||||
experimentalFeaturesEnv :: (String, String)
|
||||
experimentalFeaturesEnv =
|
||||
( "HASURA_GRAPHQL_EXPERIMENTAL_FEATURES",
|
||||
"Comma separated list of experimental features. (all: inherited_roles,optimize_permission_filters and naming_convention, streaming_subscriptions). "
|
||||
"Comma separated list of experimental features. (all: inherited_roles,optimize_permission_filters and naming_convention, streaming_subscriptions, apollo_federation). "
|
||||
<> "optimize_permission_filters: Use experimental SQL optimization"
|
||||
<> "transformations for permission filters. "
|
||||
<> "inherited_roles: ignored; inherited roles cannot be switched off"
|
||||
<> "naming_convention: apply naming convention (graphql-default/hasura-default) based on source customization"
|
||||
<> "apollo_federation: use hasura as a subgraph in an Apollo gateway"
|
||||
-- TODO(SOLOMON): Write a description of this experimental feature:
|
||||
-- <> "streaming_subscriptions: ..."
|
||||
)
|
||||
|
@ -170,10 +170,11 @@ instance FromEnv [ExperimentalFeature] where
|
||||
"streaming_subscriptions" -> Right EFStreamingSubscriptions
|
||||
"optimize_permission_filters" -> Right EFOptimizePermissionFilters
|
||||
"naming_convention" -> Right EFNamingConventions
|
||||
"apollo_federation" -> Right EFApolloFederation
|
||||
_ ->
|
||||
Left $
|
||||
"Only expecting list of comma separated experimental features, options are:"
|
||||
++ "inherited_roles, streaming_subscriptions, optimize_permission_filters, naming_convention"
|
||||
++ "inherited_roles, streaming_subscriptions, optimize_permission_filters, naming_convention, apollo_federation"
|
||||
|
||||
instance FromEnv ES.BatchSize where
|
||||
fromEnv s = do
|
||||
|
@ -70,6 +70,7 @@ data ExperimentalFeature
|
||||
| EFOptimizePermissionFilters
|
||||
| EFNamingConventions
|
||||
| EFStreamingSubscriptions
|
||||
| EFApolloFederation
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Hashable ExperimentalFeature
|
||||
@ -80,7 +81,8 @@ instance FromJSON ExperimentalFeature where
|
||||
"optimize_permission_filters" -> pure EFOptimizePermissionFilters
|
||||
"naming_convention" -> pure EFNamingConventions
|
||||
"streaming_subscriptions" -> pure EFStreamingSubscriptions
|
||||
_ -> fail "ExperimentalFeature can only be one of these value: inherited_roles, optimize_permission_filters, naming_convention or streaming_subscriptions"
|
||||
"apollo_federation" -> pure EFApolloFederation
|
||||
_ -> fail "ExperimentalFeature can only be one of these value: inherited_roles, optimize_permission_filters, naming_convention, streaming_subscriptions or apollo_federation"
|
||||
|
||||
instance ToJSON ExperimentalFeature where
|
||||
toJSON = \case
|
||||
@ -88,6 +90,7 @@ instance ToJSON ExperimentalFeature where
|
||||
EFOptimizePermissionFilters -> "optimize_permission_filters"
|
||||
EFNamingConventions -> "naming_convention"
|
||||
EFStreamingSubscriptions -> "streaming_subscriptions"
|
||||
EFApolloFederation -> "apollo_federation"
|
||||
|
||||
data MaintenanceMode a = MaintenanceModeEnabled a | MaintenanceModeDisabled
|
||||
deriving (Show, Eq)
|
||||
|
@ -38,12 +38,12 @@ fakeInputFieldValue (InputFieldInfo t _) = go t
|
||||
go :: forall k. ('Input <: k) => Type k -> G.Value Variable
|
||||
go = \case
|
||||
TList _ t' -> G.VList [go t', go t']
|
||||
TNamed _ (Definition name _ _ info) -> case (info, subKind @'Input @k) of
|
||||
TNamed _ (Definition name _ _ _ info) -> case (info, subKind @'Input @k) of
|
||||
(TIScalar, _) -> fakeScalar name
|
||||
(TIEnum ei, _) -> G.VEnum $ G.EnumValue $ dName $ NE.head ei
|
||||
(TIInputObject (InputObjectInfo oi), _) -> G.VObject $
|
||||
M.fromList $ do
|
||||
Definition fieldName _ _ fieldInfo <- oi
|
||||
Definition fieldName _ _ _ fieldInfo <- oi
|
||||
pure (fieldName, fakeInputFieldValue fieldInfo)
|
||||
_ -> error "fakeInputFieldValue: non-exhaustive. FIXME"
|
||||
|
||||
@ -51,5 +51,5 @@ fakeDirective :: DirectiveInfo -> G.Directive Variable
|
||||
fakeDirective DirectiveInfo {..} =
|
||||
G.Directive diName $
|
||||
M.fromList $
|
||||
diArguments <&> \(Definition argName _ _ argInfo) ->
|
||||
diArguments <&> \(Definition argName _ _ _ argInfo) ->
|
||||
(argName, fakeInputFieldValue argInfo)
|
||||
|
@ -146,7 +146,8 @@ mkParser table cib =
|
||||
_tciViewInfo = Nothing,
|
||||
_tciEnumValues = Nothing,
|
||||
_tciCustomConfig = tableConfig,
|
||||
_tciExtraTableMetadata = ()
|
||||
_tciExtraTableMetadata = (),
|
||||
_tciApolloFederationConfig = Nothing
|
||||
}
|
||||
|
||||
pk :: Maybe (PrimaryKey PG (ColumnInfo PG))
|
||||
|
28
server/tests-py/queries/apollo_federation/entities.yaml
Normal file
28
server/tests-py/queries/apollo_federation/entities.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
description: Introspection to check query fields and their types
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
query EntitiesTest($representations: [_Any!]!) {
|
||||
_entities(representations: $representations) {
|
||||
... on user {
|
||||
id
|
||||
email
|
||||
name
|
||||
is_admin
|
||||
}
|
||||
}
|
||||
}
|
||||
variables:
|
||||
representations:
|
||||
- __typename: user
|
||||
id: 1
|
||||
|
||||
|
||||
response:
|
||||
data:
|
||||
_entities:
|
||||
- id: 1
|
||||
email: foo@email.com
|
||||
name: foo
|
||||
is_admin: false
|
68
server/tests-py/queries/apollo_federation/root_fields.yaml
Normal file
68
server/tests-py/queries/apollo_federation/root_fields.yaml
Normal file
@ -0,0 +1,68 @@
|
||||
description: Introspection to check query fields and their types
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
__schema {
|
||||
queryType {
|
||||
fields {
|
||||
name
|
||||
type {
|
||||
name
|
||||
kind
|
||||
ofType {
|
||||
name
|
||||
kind
|
||||
ofType {
|
||||
name
|
||||
kind
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response:
|
||||
data:
|
||||
__schema:
|
||||
queryType:
|
||||
fields:
|
||||
- name: _entities
|
||||
type:
|
||||
name: _Entity
|
||||
kind: UNION
|
||||
ofType:
|
||||
- name: _service
|
||||
type:
|
||||
name:
|
||||
kind: NON_NULL
|
||||
ofType:
|
||||
name: _Service
|
||||
kind: OBJECT
|
||||
ofType:
|
||||
- name: user
|
||||
type:
|
||||
name:
|
||||
kind: NON_NULL
|
||||
ofType:
|
||||
name:
|
||||
kind: LIST
|
||||
ofType:
|
||||
name:
|
||||
kind: NON_NULL
|
||||
- name: user_aggregate
|
||||
type:
|
||||
name:
|
||||
kind: NON_NULL
|
||||
ofType:
|
||||
name: user_aggregate
|
||||
kind: OBJECT
|
||||
ofType:
|
||||
- name: user_by_pk
|
||||
type:
|
||||
name: user
|
||||
kind: OBJECT
|
||||
ofType:
|
24
server/tests-py/queries/apollo_federation/setup.yaml
Normal file
24
server/tests-py/queries/apollo_federation/setup.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
CREATE TABLE "user"(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
INSERT INTO "user" (id, name, email) VALUES
|
||||
(1, 'foo', 'foo@email.com'),
|
||||
(2, 'bar', 'bar@email.com'),
|
||||
(3, 'bar', 'bar@email.com'),
|
||||
(4, 'baz', 'baz@email.com');
|
||||
|
||||
- type: track_table
|
||||
args:
|
||||
table: user
|
||||
schema: public
|
||||
apollo_federation_config:
|
||||
enable: v1
|
7
server/tests-py/queries/apollo_federation/teardown.yaml
Normal file
7
server/tests-py/queries/apollo_federation/teardown.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: run_sql
|
||||
args:
|
||||
cascade: true
|
||||
sql: |
|
||||
DROP TABLE "user";
|
@ -0,0 +1,21 @@
|
||||
const { ApolloServer } = require('apollo-server');
|
||||
const { ApolloGateway } = require("@apollo/gateway");
|
||||
|
||||
const gateway = new ApolloGateway({
|
||||
serviceList: [
|
||||
{ name: 'hge', url: process.env.HGE_URL + "/v1/graphql" },
|
||||
{ name: 'other', url: 'http://localhost:4003/' }
|
||||
],
|
||||
introspectionHeaders: {
|
||||
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET
|
||||
}
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
gateway,
|
||||
subscriptions: false
|
||||
});
|
||||
|
||||
server.listen(4004).then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
const { ApolloServer } = require('apollo-server');
|
||||
const { ApolloGateway } = require("@apollo/gateway");
|
||||
|
||||
const gateway = new ApolloGateway({
|
||||
serviceList: [
|
||||
{ name: 'hge', url: process.env.HGE_URL + "/v1/graphql" }
|
||||
],
|
||||
introspectionHeaders: {
|
||||
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET
|
||||
}
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
gateway,
|
||||
subscriptions: false
|
||||
});
|
||||
|
||||
server.listen({ port: process.env.PORT || 4002 }).then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
54
server/tests-py/remote_schemas/nodejs/apollo_server_1.js
Normal file
54
server/tests-py/remote_schemas/nodejs/apollo_server_1.js
Normal file
@ -0,0 +1,54 @@
|
||||
const { ApolloServer, gql } = require('apollo-server');
|
||||
const { buildSubgraphSchema } = require('@apollo/subgraph');
|
||||
|
||||
const user = [
|
||||
{
|
||||
id: 1,
|
||||
city: 'New York'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
city: 'Bangalore'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
city: 'Melbourne'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
city: 'New Delhi'
|
||||
}
|
||||
];
|
||||
|
||||
const typeDefs = gql`
|
||||
extend schema
|
||||
@link(url: "https://specs.apollo.dev/federation/v2.0",
|
||||
import: ["@key", "@extends", "@external", "@shareable"])
|
||||
|
||||
type Query {
|
||||
getUserData(id: Int!): user
|
||||
}
|
||||
|
||||
type user @key(fields: "id") @extends {
|
||||
id: Int! @external
|
||||
city: String
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
getUserData(parent, args, context, info) {
|
||||
return user.find(user => user.id === args.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema: buildSubgraphSchema({ typeDefs, resolvers })
|
||||
});
|
||||
|
||||
server.listen(4003).then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
2852
server/tests-py/remote_schemas/nodejs/package-lock.json
generated
2852
server/tests-py/remote_schemas/nodejs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,8 +10,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"apollo-server": "2.1.0",
|
||||
"graphql": "14.2.1",
|
||||
"graphql-tag": "2.10.1"
|
||||
"apollo-server": "3.8.1",
|
||||
"@apollo/gateway": "2.0.3",
|
||||
"graphql": "16.5.0",
|
||||
"graphql-tag": "2.10.1",
|
||||
"@apollo/subgraph": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
87
server/tests-py/test_apollo_federation.py
Normal file
87
server/tests-py/test_apollo_federation.py
Normal file
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import requests
|
||||
from remote_server import NodeGraphQL
|
||||
from validate import check_query_f
|
||||
|
||||
|
||||
def make_request(url, query):
|
||||
print('Sending request to the local federated server')
|
||||
payload = {'query': query}
|
||||
resp = requests.post(url, json=payload)
|
||||
return resp
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.getenv('HASURA_GRAPHQL_EXPERIMENTAL_FEATURES') is None or
|
||||
not 'apollo_federation' in os.getenv('HASURA_GRAPHQL_EXPERIMENTAL_FEATURES'),
|
||||
reason="This test expects the (apollo_federation) experimental feature turned on")
|
||||
@pytest.mark.usefixtures('per_class_tests_db_state')
|
||||
class TestApolloFederation:
|
||||
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return 'queries/apollo_federation'
|
||||
|
||||
def test_apollo_federated_server_with_hge_only(self,hge_ctx):
|
||||
# start the node server
|
||||
fed_server = NodeGraphQL(["node", "remote_schemas/nodejs/apollo_federated_server_with_hge_only.js"])
|
||||
fed_server.start()
|
||||
|
||||
url = 'http://localhost:4002'
|
||||
|
||||
# run a GQL query
|
||||
gql_query = """
|
||||
query {
|
||||
user_by_pk(id: 1) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
"""
|
||||
resp = make_request(url, gql_query)
|
||||
|
||||
# stop the node server
|
||||
fed_server.stop()
|
||||
|
||||
# check if everything was okay
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert 'data' in resp.text
|
||||
|
||||
def test_apollo_federated_server_with_hge_and_apollo_graphql_server(self,hge_ctx):
|
||||
# start the node servers
|
||||
server_1 = NodeGraphQL(["node", "remote_schemas/nodejs/apollo_server_1.js"])
|
||||
fed_server = NodeGraphQL(["node", "remote_schemas/nodejs/apollo_federated_server_with_hge_and_server1.js"])
|
||||
|
||||
server_1.start()
|
||||
fed_server.start()
|
||||
|
||||
url = 'http://localhost:4004'
|
||||
|
||||
# run a GQL query
|
||||
gql_query = """
|
||||
query {
|
||||
getUserData(id: 1) {
|
||||
id
|
||||
name
|
||||
city
|
||||
email
|
||||
}
|
||||
}
|
||||
"""
|
||||
resp = make_request(url, gql_query)
|
||||
|
||||
# stop the node servers
|
||||
fed_server.stop()
|
||||
server_1.stop()
|
||||
|
||||
# check if everything was okay
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert 'data' in resp.text
|
||||
|
||||
def test_apollo_federation_fields(self,hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/root_fields.yaml')
|
||||
|
||||
def test_apollo_federation_entities(self,hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/entities.yaml')
|
Loading…
Reference in New Issue
Block a user