Skip to content

Commit

Permalink
Dmoverton/5863 prefix namespacing
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 108e8b2
  • Loading branch information
dmoverton authored and hasura-bot committed Jun 9, 2021
1 parent 972c662 commit 4a69fde
Show file tree
Hide file tree
Showing 45 changed files with 4,067 additions and 257 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)
- server: fix add source API wiping out source's metadata when replace_configuration is true
- server: add support for customization of field names and type names when adding a remote schema
- console: add foreign key CRUD functionality to ms sql server tables
- console: allow tracking of custom SQL functions having composite type (rowtype) input arguments
- console: allow input object presets in remote schema permissions
Expand Down
53 changes: 53 additions & 0 deletions rfcs/remote-schema-customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Remote Schema Customization

## Motivation

- Prevent name conflicts between remote schemas and other sources
- Allow customization of third-party schemas to better fit with local naming conventions

## Spec

Add an optional `customization` object to the `add_remote_schema` API with the following form:

```yaml
customization:
# if root_fields_namespace is absent, the fields
# are merged into the query root directly
root_fields_namespace: "something"
type_names:
prefix: some_prefix
suffix: some_suffix
# mapping takes precedence over prefix and suffix
mapping:
old_name: new_name
field_names:
- parent_type: old_type_name
prefix: some_prefix
suffix: some_suffix
# mapping takes precedence over prefix and suffix
mapping:
old_name: new_name
```
- Type name prefix and suffix will be applied to all types in the schema except the root types (for query, mutation and subscription), types starting with `__`, standard scalar types (`Int`, `Float`, `String`, `Boolean`, and `ID`), and types with an explicit mapping.
- Root types, types starting with `__` and standard scalar types may only be customized with an explicit mapping.
- Fields that are part of an interface must be renamed consistently across all object types that implement that interface

## Implementation approach

- After obtaining the remote schema via introspection we build customization functions (and their inverses) for the types and fields in the schema.
- Customizations are validated against the schema to ensure that:
- Field renamings of objects and interfaces are consistent
- Customization does not result in two types, or two fields of the same type, being renamed to the same name.
- The remote schema is customized using the customization functions to rename types and fields.
- The field parser generators are modified so that they recognise the customized schema, but generate GraphQL queries with the original names to be sent to the remote server
- We can uses aliases on the fields to make sure the response object has the customized fields names.

## Open questions

- Do we want to allow mapping of field names for input object types? I have assumed that we don't.
- Do we need to do anything special for remote joins?

## Future work

- Handle mapping of responses to introspection queries, e.g. `__typename` field.
4 changes: 4 additions & 0 deletions server/graphql-engine.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ library
, Hasura.RQL.Types.Run
, Hasura.Session

, Test.QuickCheck.Arbitrary.Extended

-- exposed for Pro
, Hasura.Server.API.Config
, Hasura.Server.Telemetry
Expand Down Expand Up @@ -664,6 +666,8 @@ test-suite graphql-engine-tests
Hasura.GraphQL.Parser.TestUtils
Hasura.GraphQL.Schema.RemoteTest
Hasura.IncrementalSpec
Hasura.SessionSpec
Hasura.GraphQL.RemoteServerSpec
Hasura.RQL.MetadataSpec
Hasura.RQL.Types.EndpointSpec
Hasura.SQL.WKTSpec
Expand Down
4 changes: 4 additions & 0 deletions server/src-lib/Data/Aeson/Ordered.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Data.Aeson.Ordered
, array
, insert
, delete
, adjust
, empty
, eitherDecode
, toEncJSON
Expand Down Expand Up @@ -127,6 +128,9 @@ lookup key (Object_ omap) = OMap.lookup key omap
delete :: Text -> Object -> Object
delete key (Object_ omap) = Object_ (OMap.delete key omap)

adjust :: (Value -> Value) -> Text -> Object -> Object
adjust f key (Object_ omap) = Object_ (OMap.adjust f key omap)

-- | ToList a key.
toList :: Object -> [(Text,Value)]
toList (Object_ omap) = OMap.toList omap
Expand Down
1 change: 1 addition & 0 deletions server/src-lib/Data/Environment.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Hasura.Prelude
import qualified Data.Map as M
import qualified System.Environment

-- | Server process environment variables
newtype Environment = Environment (M.Map String String) deriving (Eq, Show, Generic)

instance FromJSON Environment
Expand Down
4 changes: 4 additions & 0 deletions server/src-lib/Data/List/Extended.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Data.List.Extended
, getDifference
, getDifferenceOn
, getOverlapWith
, hasNoDuplicates
, module L
) where

Expand Down Expand Up @@ -34,3 +35,6 @@ getOverlapWith getKey left right =
Map.elems $ Map.intersectionWith (,) (mkMap left) (mkMap right)
where
mkMap = Map.fromList . map (\v -> (getKey v, v))

hasNoDuplicates :: (Eq a, Hashable a) => [a] -> Bool
hasNoDuplicates xs = Set.size (Set.fromList xs) == length xs
47 changes: 23 additions & 24 deletions server/src-lib/Hasura/Backends/BigQuery/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ data JoinSource
= JoinSelect Select
-- We're not using existingJoins at the moment, which was used to
-- avoid re-joining on the same table twice.
-- | JoinReselect Reselect
deriving (Eq, Show, Generic, Data, Lift)
instance FromJSON JoinSource
instance Hashable JoinSource
Expand Down Expand Up @@ -354,7 +353,7 @@ instance ToJSONKey TableName
instance NFData TableName
instance Arbitrary TableName where arbitrary = genericArbitrary

instance ToTxt TableName where toTxt = T.pack . show
instance ToTxt TableName where toTxt = tshow

data FieldName = FieldName
{ fieldName :: Text
Expand Down Expand Up @@ -388,22 +387,22 @@ data Op
| LessOrEqualOp
| MoreOp
| MoreOrEqualOp
-- | SIN
-- | SNE
-- | SLIKE
-- | SNLIKE
-- | SILIKE
-- | SNILIKE
-- | SSIMILAR
-- | SNSIMILAR
-- | SGTE
-- | SLTE
-- | SNIN
-- | SContains
-- | SContainedIn
-- | SHasKey
-- | SHasKeysAny
-- | SHasKeysAll
-- SIN
-- SNE
-- SLIKE
-- SNLIKE
-- SILIKE
-- SNILIKE
-- SSIMILAR
-- SNSIMILAR
-- SGTE
-- SLTE
-- SNIN
-- SContains
-- SContainedIn
-- SHasKey
-- SHasKeysAny
-- SHasKeysAll
deriving (Eq, Show, Generic, Data, Lift)
instance FromJSON Op
instance Hashable Op
Expand Down Expand Up @@ -475,7 +474,7 @@ instance FromJSON Int64 where parseJSON = liberalInt64Parser Int64
instance ToJSON Int64 where toJSON = liberalIntegralPrinter

intToInt64 :: Int -> Int64
intToInt64 = Int64 . T.pack . show
intToInt64 = Int64 . tshow

-- | BigQuery's conception of a fixed precision decimal.
newtype Decimal = Decimal Text
Expand Down Expand Up @@ -542,7 +541,7 @@ instance ToJSON ScalarType
instance ToJSONKey ScalarType
instance NFData ScalarType
instance Hashable ScalarType
instance ToTxt ScalarType where toTxt = T.pack . show
instance ToTxt ScalarType where toTxt = tshow

--------------------------------------------------------------------------------
-- Unified table metadata
Expand Down Expand Up @@ -675,7 +674,7 @@ liberalInt64Parser fromText json = viaText <|> viaNumber
_ -> fail ("String containing integral number is invalid: " ++ show text)
viaNumber = do
int <- J.parseJSON json
pure (fromText (T.pack (show (int :: Int))))
pure (fromText (tshow (int :: Int)))

-- | Parse either a JSON native double number, or a text string
-- containing something vaguely in scientific notation. In either
Expand All @@ -688,11 +687,11 @@ liberalDecimalParser fromText json = viaText <|> viaNumber
-- Parsing scientific is safe; it doesn't normalise until we ask
-- it to.
case readP_to_S scientificP (T.unpack text) of
[(_)] -> pure (fromText text)
_ -> fail ("String containing decimal places is invalid: " ++ show text)
[_] -> pure (fromText text)
_ -> fail ("String containing decimal places is invalid: " ++ show text)
viaNumber = do
d <- J.parseJSON json
-- Converting a scientific to an unbounded number is unsafe, but
-- to a double is bounded and therefore OK. JSON only supports
-- doubles, so that's fine.
pure (fromText (T.pack (show (d :: Double))))
pure (fromText (tshow (d :: Double)))
2 changes: 1 addition & 1 deletion server/src-lib/Hasura/Backends/Postgres/DDL/Field.hs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ buildRemoteFieldInfo remoteRelationship
pgColumns
remoteSchemaMap = do
let remoteSchemaName = rtrRemoteSchema remoteRelationship
(RemoteSchemaCtx _name introspectionResult remoteSchemaInfo _ _ _permissions) <-
(RemoteSchemaCtx _name introspectionResult remoteSchemaInfo _ _ _ _ _permissions) <-
onNothing (Map.lookup remoteSchemaName remoteSchemaMap)
$ throw400 RemoteSchemaError $ "remote schema with name " <> remoteSchemaName <<> " not found"
eitherRemoteField <- runExceptT $
Expand Down
21 changes: 19 additions & 2 deletions server/src-lib/Hasura/GraphQL/Context.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Hasura.GraphQL.Context
, MutationDB(..)
, ActionQuery(..)
, ActionMutation(..)
, RemoteRootField(..)
, getRemoteFieldSelectionSet
, RemoteFieldG (..)
, RemoteField
, QueryRootField
Expand Down Expand Up @@ -103,10 +105,25 @@ data ActionMutation (b :: BackendType) v
= AMSync !(RQL.AnnActionExecution b v)
| AMAsync !RQL.AnnActionMutationAsync

-- | An RemoteRootField could either be a real field on the remote server
-- or represent a virtual namespace that only exists in the Hasura schema.
data RemoteRootField var
= RRFNamespaceField !(G.SelectionSet G.NoFragments var) -- ^ virtual namespace field
| RRFRealField !(G.Field G.NoFragments var) -- ^ a real field on the remote server
deriving (Functor, Foldable, Traversable)

-- | For a real remote field gives a SelectionSet for selecting the field itself.
-- For a virtual field gives the unwrapped SelectionSet for the field.
getRemoteFieldSelectionSet :: RemoteRootField var -> G.SelectionSet G.NoFragments var
getRemoteFieldSelectionSet = \case
RRFNamespaceField selSet -> selSet
RRFRealField fld -> [G.SelectionField fld]

data RemoteFieldG var
= RemoteFieldG
{ _rfRemoteSchemaInfo :: !RQL.RemoteSchemaInfo
, _rfField :: !(G.Field G.NoFragments var)
{ _rfRemoteSchemaInfo :: !RQL.RemoteSchemaInfo
, _rfTypeNameCustomizer :: !(Maybe (G.Name -> G.Name))
, _rfField :: !(RemoteRootField var)
} deriving (Functor, Foldable, Traversable)

type RemoteField = RemoteFieldG RQL.RemoteSchemaVariable
Expand Down
55 changes: 5 additions & 50 deletions server/src-lib/Hasura/GraphQL/Execute.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module Hasura.GraphQL.Execute
, checkQueryInAllowlist
, MultiplexedLiveQueryPlan(..)
, LiveQueryPlan (..)
, getQueryParts -- this function is exposed for testing in parameterized query hash
) where

import Hasura.Prelude
Expand All @@ -28,13 +27,10 @@ import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai.Extended as Wai

import Data.Text.Extended

import qualified Hasura.GraphQL.Context as C
import qualified Hasura.GraphQL.Execute.Action as EA
import qualified Hasura.GraphQL.Execute.Backend as EB
import qualified Hasura.GraphQL.Execute.Common as EC
import qualified Hasura.GraphQL.Execute.Inline as EI
import qualified Hasura.GraphQL.Execute.LiveQuery.Plan as EL
import qualified Hasura.GraphQL.Execute.Mutation as EM
import qualified Hasura.GraphQL.Execute.Prepare as EPr
Expand All @@ -60,8 +56,6 @@ import Hasura.Server.Version (HasVersion)
import Hasura.Session


type QueryParts = G.TypedOperationDefinition G.FragmentSpread G.Name

-- | Execution context
data ExecutionCtx
= ExecutionCtx
Expand Down Expand Up @@ -109,40 +103,15 @@ instance MonadGQLExecutionCheck m => MonadGQLExecutionCheck (MetadataStorageT m)
checkGQLExecution ui det enableAL sc req =
lift $ checkGQLExecution ui det enableAL sc req

-- | Depending on the request parameters, fetch the correct typed operation
-- definition from the GraphQL query
getQueryParts
:: MonadError QErr m
=> GQLReqParsed
-> m QueryParts
getQueryParts (GQLReq opNameM q _varValsM) = do
let (selSets, opDefs, _fragDefsL) = G.partitionExDefs $ unGQLExecDoc q
case (opNameM, selSets, opDefs) of
(Just opName, [], _) -> do
let n = _unOperationName opName
opDefM = find (\opDef -> G._todName opDef == Just n) opDefs
onNothing opDefM $ throw400 ValidationFailed $
"no such operation found in the document: " <> dquote n
(Just _, _, _) ->
throw400 ValidationFailed $ "operationName cannot be used when " <>
"an anonymous operation exists in the document"
(Nothing, [selSet], []) ->
return $ G.TypedOperationDefinition G.OperationTypeQuery Nothing [] [] selSet
(Nothing, [], [opDef]) ->
return opDef
(Nothing, _, _) ->
throw400 ValidationFailed $ "exactly one operation has to be present " <>
"in the document when operationName is not specified"

getExecPlanPartial
:: (MonadError QErr m)
=> UserInfo
-> SchemaCache
-> ET.GraphQLQueryType
-> GQLReqParsed
-> m (C.GQLContext, QueryParts)
-> m (C.GQLContext, SingleOperation)
getExecPlanPartial userInfo sc queryType req =
(getGCtx ,) <$> getQueryParts req
(getGCtx ,) <$> getSingleOperation req
where
role = _uiRole userInfo

Expand All @@ -165,7 +134,6 @@ getExecPlanPartial userInfo sc queryType req =
BOFAAllowed -> fromMaybe frontend backend
BOFADisallowed -> frontend


-- The graphql query is resolved into a sequence of execution operations
data ResolvedExecutionPlan
= QueryExecutionPlan EB.ExecutionPlan [C.QueryRootField UnpreparedValue]
Expand Down Expand Up @@ -324,36 +292,23 @@ getResolvedExecPlan env logger {- planCache-} userInfo sqlGenCtx
-- opNameM queryStr plan planCache
noExistingPlan :: m (G.SelectionSet G.NoFragments Variable, ResolvedExecutionPlan)
noExistingPlan = do
-- GraphQL requests may incorporate fragments which insert a pre-defined
-- part of a GraphQL query. Here we make sure to remember those
-- pre-defined sections, so that when we encounter a fragment spread
-- later, we can inline it instead.
let takeFragment = \case G.ExecutableDefinitionFragment f -> Just f; _ -> Nothing
fragments =
mapMaybe takeFragment $ unGQLExecDoc $ _grQuery reqParsed
(gCtx, queryParts) <- getExecPlanPartial userInfo sc queryType reqParsed

case queryParts of
G.TypedOperationDefinition G.OperationTypeQuery _ varDefs directives selSet -> do
-- (Here the above fragment inlining is actually executed.)
inlinedSelSet <- EI.inlineSelectionSet fragments selSet
G.TypedOperationDefinition G.OperationTypeQuery _ varDefs directives inlinedSelSet -> do
(executionPlan, queryRootFields, normalizedSelectionSet) <-
EQ.convertQuerySelSet env logger gCtx userInfo httpManager reqHeaders directives inlinedSelSet varDefs (_grVariables reqUnparsed) (scSetGraphqlIntrospectionOptions sc)
pure $ (normalizedSelectionSet, QueryExecutionPlan executionPlan queryRootFields)

-- See Note [Temporarily disabling query plan caching]
-- traverse_ (addPlanToCache . EP.RPQuery) plan
G.TypedOperationDefinition G.OperationTypeMutation _ varDefs directives selSet -> do
-- (Here the above fragment inlining is actually executed.)
inlinedSelSet <- EI.inlineSelectionSet fragments selSet
G.TypedOperationDefinition G.OperationTypeMutation _ varDefs directives inlinedSelSet -> do
(executionPlan, normalizedSelectionSet) <-
EM.convertMutationSelectionSet env logger gCtx sqlGenCtx userInfo httpManager reqHeaders directives inlinedSelSet varDefs (_grVariables reqUnparsed) (scSetGraphqlIntrospectionOptions sc)
pure $ (normalizedSelectionSet, MutationExecutionPlan executionPlan)
-- See Note [Temporarily disabling query plan caching]
-- traverse_ (addPlanToCache . EP.RPQuery) plan
G.TypedOperationDefinition G.OperationTypeSubscription _ varDefs directives selSet -> do
-- (Here the above fragment inlining is actually executed.)
inlinedSelSet <- EI.inlineSelectionSet fragments selSet
G.TypedOperationDefinition G.OperationTypeSubscription _ varDefs directives inlinedSelSet -> do
-- Parse as query to check correctness
(unpreparedAST, _reusability, normalizedDirectives, normalizedSelectionSet) <-
EQ.parseGraphQLQuery gCtx varDefs (_grVariables reqUnparsed) directives inlinedSelSet
Expand Down
Loading

0 comments on commit 4a69fde

Please sign in to comment.