The following objects in your metadata are inconsistent because
- they reference database or Remote-Schema entities which do not
- seem to exist
+ they reference database or remote-schema entities which do not
+ seem to exist or are conflicting
The GraphQL API has been generated using only the consistent parts
@@ -153,8 +150,8 @@ const MetadataStatus = ({
To resolve these inconsistencies, you can do one of the following:
- To delete all the inconsistent objects, click the "Delete all"
- button
+ To delete all the inconsistent objects from the metadata, click
+ the "Delete all" button
If you want to manage these objects on your own, please do so and
diff --git a/console/src/components/Services/Metadata/utils.js b/console/src/components/Services/Metadata/utils.js
index eefbdaf0f61a0..d83388330dffd 100644
--- a/console/src/components/Services/Metadata/utils.js
+++ b/console/src/components/Services/Metadata/utils.js
@@ -12,62 +12,105 @@ export const getTableNameFromDef = def => {
return def;
};
-export const filterSchema = (metadata, inconsistentObject, type) => {
- if (type === 'tables') {
- const schemas = metadata;
- if (inconsistentObject.type === 'table') {
- const tableName = getTableNameFromDef(inconsistentObject.definition);
- return schemas.filter(s => s.table_name !== tableName);
- }
- if (
- inconsistentObject.type === 'array_relation' ||
- inconsistentObject.type === 'object_relation'
- ) {
- const { table } = inconsistentObject.definition;
- return schemas.map(schema => {
- if (schema.table_name === getTableNameFromDef(table)) {
- return {
- ...schema,
- relationships: schema.relationships.filter(r => {
- return r.rel_name !== inconsistentObject.definition.name;
- }),
- };
- }
- return schema;
- });
- }
- if (permissionTypes.includes(inconsistentObject.type)) {
- const { table } = inconsistentObject.definition;
- return schemas.map(schema => {
- if (schema.table_name === getTableNameFromDef(table)) {
- return {
- ...schema,
- permissions: schema.permissions.filter(p => {
- return p.role_name !== inconsistentObject.definition.role;
- }),
- };
- }
- return schema;
- });
- }
- return schemas;
- } else if (type === 'functions') {
- const functions = metadata;
- if (inconsistentObject.type === 'function') {
- return functions.filter(
- f =>
- f.function_name !== getTableNameFromDef(inconsistentObject.definition)
- );
- }
- return functions;
- } else if (type === 'events') {
- const triggers = metadata;
- if (inconsistentObject.type === 'event_trigger') {
- return triggers.filter(
- t => t.name !== inconsistentObject.definition.configuration.name
- );
- }
- return triggers;
+const filterInconsistentMetadataObject = (
+ objects,
+ inconsistentObject,
+ type
+) => {
+ switch (type) {
+ case 'tables':
+ const schemas = objects;
+
+ if (inconsistentObject.type === 'table') {
+ const tableName = getTableNameFromDef(inconsistentObject.definition);
+ return schemas.filter(s => s.table_name !== tableName);
+ }
+
+ if (
+ inconsistentObject.type === 'array_relation' ||
+ inconsistentObject.type === 'object_relation'
+ ) {
+ const { table } = inconsistentObject.definition;
+ return schemas.map(schema => {
+ if (schema.table_name === getTableNameFromDef(table)) {
+ return {
+ ...schema,
+ relationships: schema.relationships.filter(r => {
+ return r.rel_name !== inconsistentObject.definition.name;
+ }),
+ };
+ }
+ return schema;
+ });
+ }
+
+ if (permissionTypes.includes(inconsistentObject.type)) {
+ const { table } = inconsistentObject.definition;
+ return schemas.map(schema => {
+ if (schema.table_name === getTableNameFromDef(table)) {
+ return {
+ ...schema,
+ permissions: schema.permissions.filter(p => {
+ return p.role_name !== inconsistentObject.definition.role;
+ }),
+ };
+ }
+ return schema;
+ });
+ }
+
+ return schemas;
+ case 'functions':
+ const functions = objects;
+
+ if (inconsistentObject.type === 'function') {
+ return functions.filter(
+ f =>
+ f.function_name !==
+ getTableNameFromDef(inconsistentObject.definition)
+ );
+ }
+
+ return functions;
+ case 'events':
+ const triggers = objects;
+
+ if (inconsistentObject.type === 'event_trigger') {
+ return triggers.filter(
+ t => t.name !== inconsistentObject.definition.configuration.name
+ );
+ }
+
+ return triggers;
+ case 'remote_schemas':
+ const remoteSchemas = objects;
+
+ if (inconsistentObject.type === 'remote_schema') {
+ return remoteSchemas.filter(
+ t => t.name !== inconsistentObject.definition.name
+ );
+ }
+
+ return remoteSchemas;
+ default:
+ return objects;
}
- return metadata;
+};
+
+export const filterInconsistentMetadataObjects = (
+ metadataObjects,
+ inconsistentObjects,
+ type
+) => {
+ let filteredMetadataObjects = JSON.parse(JSON.stringify(metadataObjects));
+
+ inconsistentObjects.forEach(object => {
+ filteredMetadataObjects = filterInconsistentMetadataObject(
+ filteredMetadataObjects,
+ object,
+ type
+ );
+ });
+
+ return filteredMetadataObjects;
};
diff --git a/console/src/helpers/versionUtils.js b/console/src/helpers/versionUtils.js
index dee7cd19da1ab..d36a99524c59d 100644
--- a/console/src/helpers/versionUtils.js
+++ b/console/src/helpers/versionUtils.js
@@ -1,10 +1,12 @@
const semver = require('semver');
export const FT_JWT_ANALYZER = 'JWTAnalyzer';
+export const RELOAD_METADATA_API_CHANGE = 'reloadMetaDataApiChange';
// list of feature launch versions
const featureLaunchVersions = {
// feature: 'v1.0.0'
+ [RELOAD_METADATA_API_CHANGE]: 'v1.0.0-beta.3',
[FT_JWT_ANALYZER]: 'v1.0.0-beta.3',
};
diff --git a/docs/graphql/manual/api-reference/schema-metadata-api/index.rst b/docs/graphql/manual/api-reference/schema-metadata-api/index.rst
index 275bda6b1e362..c4c4880496e5c 100644
--- a/docs/graphql/manual/api-reference/schema-metadata-api/index.rst
+++ b/docs/graphql/manual/api-reference/schema-metadata-api/index.rst
@@ -152,13 +152,25 @@ The various types of queries are listed in the following table:
- :ref:`create_event_trigger_args `
- Create or replace event trigger
+ * - :ref:`invoke_event_trigger`
+ - :ref:`invoke_event_trigger_args `
+ - Invoke trigger manually
+
* - :ref:`delete_event_trigger`
- :ref:`delete_event_trigger_args `
- Delete existing event trigger
- * - :ref:`invoke_event_trigger`
- - :ref:`invoke_event_trigger_args `
- - Invoke trigger manually
+ * - :ref:`add_remote_schema`
+ - :ref:`add_remote_schema_args `
+ - Add a remote GraphQL server as remote schema
+
+ * - :ref:`remove_remote_schema`
+ - :ref:`remove_remote_schema_args `
+ - Remove existing remote schema
+
+ * - :ref:`reload_remote_schema`
+ - :ref:`reload_remote_schema_args `
+ - Reload schema of existing remote server
* - :ref:`export_metadata`
- :ref:`Empty Object`
@@ -216,6 +228,7 @@ The various types of queries are listed in the following table:
- :doc:`Relationships `
- :doc:`Permissions `
- :doc:`Event Triggers `
+- :doc:`Remote Schemas `
- :doc:`Query Collections `
- :doc:`Manage Metadata `
@@ -298,6 +311,7 @@ See :doc:`../../deployment/graphql-engine-flags/reference` for info on setting t
Relationships
Permissions
Event Triggers
+ Remote Schemas
Query Collections
Manage Metadata
Syntax definitions
diff --git a/docs/graphql/manual/api-reference/schema-metadata-api/remote-schemas.rst b/docs/graphql/manual/api-reference/schema-metadata-api/remote-schemas.rst
new file mode 100644
index 0000000000000..c8b6fbabe4d38
--- /dev/null
+++ b/docs/graphql/manual/api-reference/schema-metadata-api/remote-schemas.rst
@@ -0,0 +1,132 @@
+Schema/Metadata API Reference: Remote schemas
+=============================================
+
+.. contents:: Table of contents
+ :backlinks: none
+ :depth: 1
+ :local:
+
+Add/Remove a remote GraphQL server as remote schema in Hasura GraphQL engine.
+
+.. _add_remote_schema:
+
+add_remote_schema
+-----------------
+
+``add_remote_schema`` is used to add a remote GraphQL server as remote schema. GraphQL engine stitches it's schema with existing.
+
+An example request as follows:
+
+.. code-block:: http
+
+ POST /v1/query HTTP/1.1
+ Content-Type: application/json
+ X-Hasura-Role: admin
+
+ {
+ "type": "add_remote_schema",
+ "args": {
+ "name": "my remote schema",
+ "definition": {
+ "url": "https://remote-server.com/graphql",
+ "headers": [{"name": "X-Server-Request-From", "value": "Hasura"}],
+ "forward_client_headers": false
+ },
+ "comment": "some optional comment"
+ }
+ }
+
+
+.. _add_remote_schema_syntax:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Key
+ - Required
+ - Schema
+ - Description
+ * - name
+ - true
+ - :ref:`RemoteSchemaName`
+ - Name of the remote schema
+ * - definition
+ - true
+ - :ref:`RemoteSchemaDef`
+ - Definition for the remote schema
+ * - comment
+ - false
+ - Text
+ - comment
+
+.. _remove_remote_schema:
+
+remove_remote_schema
+--------------------
+
+``remove_remote_schema`` is used to delete a remote schema. GraphQL engine de-stitches it's schema.
+
+An example request as follows:
+
+.. code-block:: http
+
+ POST /v1/query HTTP/1.1
+ Content-Type: application/json
+ X-Hasura-Role: admin
+
+ {
+ "type": "remove_remote_schema",
+ "args": {
+ "name": "my remote schema"
+ }
+ }
+
+.. _remove_remote_schema_syntax:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Key
+ - Required
+ - Schema
+ - Description
+ * - name
+ - true
+ - :ref:`RemoteSchemaName`
+ - Name of the remote schema
+
+.. _reload_remote_schema:
+
+reload_remote_schema
+--------------------
+
+``reload_remote_schema`` is used to refresh schema of the remote server. GraphQL engine refetches schema from server and stitches.
+
+An example request as follows:
+
+.. code-block:: http
+
+ POST /v1/query HTTP/1.1
+ Content-Type: application/json
+ X-Hasura-Role: admin
+
+ {
+ "type": "reload_remote_schema",
+ "args": {
+ "name": "my remote schema"
+ }
+ }
+
+.. _reload_remote_schema_syntax:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Key
+ - Required
+ - Schema
+ - Description
+ * - name
+ - true
+ - :ref:`RemoteSchemaName`
+ - Name of the remote schema
diff --git a/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst b/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst
index b8323754dd2f0..22cf0576446d3 100644
--- a/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst
+++ b/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst
@@ -399,6 +399,35 @@ E.g. where ``id`` is derived from session variable and ``city`` is a static valu
If the value of any key begins with "x-hasura-" (*case-insensitive*), the value of the column specified in the key will be derived from a session variable of the same name.
+.. _RemoteSchemaName:
+
+RemoteSchemaName
+^^^^^^^^^^^^^^^^
+
+.. parsed-literal::
+
+ String
+
+.. _RemoteSchemaDef:
+
+RemoteSchemaDef
+^^^^^^^^^^^^^^^
+
+.. parsed-literal::
+ :class: haskell-pre
+
+ {
+ "url" : url-string,
+ "url_from_env" : env-var-string,
+ "headers": [
+ { "name": header-name-string,
+ "value": header-value-string,
+ "value_from_env": env-var-string
+ }
+ ],
+ "forward_client_headers": boolean
+ }
+
.. _CollectionName:
CollectionName
diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst
index 9ff5d3b12d36b..22235d3bbbc58 100644
--- a/docs/graphql/manual/remote-schemas/index.rst
+++ b/docs/graphql/manual/remote-schemas/index.rst
@@ -121,6 +121,17 @@ Remote schema fields nomenclature
structure* will result in type conflicts.
+Schema refreshing
+^^^^^^^^^^^^^^^^^
+
+For versions <= ``v1.0.0-beta.2``, GraphQL schema of each added remote server is refreshed every time a
+metadata modifying operation like adding tables/functions, defining relationships/permissions etc. is done.
+
+From ``v1.0.0-beta.3`` onwards, a remote server's GraphQL schema is cached and refreshed only when user
+explicitly reloads remote schema by clicking the ``Reload`` button on console or
+by making :doc:`reload_remote_schema<../api-reference/schema-metadata-api/remote-schemas>` metadata API request
+
+
Current limitations
^^^^^^^^^^^^^^^^^^^
@@ -172,9 +183,9 @@ will selected.
Cookie header from your remote GraphQL servers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``Set-Cookie`` headers from your remote schema servers are sent back to the
-client over HTTP transport. **Over websocket transport there exists no means
-to send headers after a query/mutation and hence ``Set-Cookie`` headers are
-not sent to the client.** Use HTTP transport if your remote servers set cookies.
+client over HTTP transport. **Over websocket transport there exists no means
+to send headers after a query/mutation and hence ``Set-Cookie`` headers are
+not sent to the client.** Use HTTP transport if your remote servers set cookies.
Bypassing Hasura's authorization system for remote schema queries
diff --git a/server/src-lib/Hasura/GraphQL/Context.hs b/server/src-lib/Hasura/GraphQL/Context.hs
index 5e2148c49d1a0..262a8cb0fa2d4 100644
--- a/server/src-lib/Hasura/GraphQL/Context.hs
+++ b/server/src-lib/Hasura/GraphQL/Context.hs
@@ -136,7 +136,7 @@ mkHsraObjFldInfo
-> G.GType
-> ObjFldInfo
mkHsraObjFldInfo descM name params ty =
- ObjFldInfo descM name params ty HasuraType
+ ObjFldInfo descM name params ty TLHasuraType
mkHsraObjTyInfo
:: Maybe G.Description
@@ -145,7 +145,7 @@ mkHsraObjTyInfo
-> ObjFieldMap
-> ObjTyInfo
mkHsraObjTyInfo descM ty implIFaces flds =
- mkObjTyInfo descM ty implIFaces flds HasuraType
+ mkObjTyInfo descM ty implIFaces flds TLHasuraType
mkHsraInpTyInfo
:: Maybe G.Description
@@ -153,7 +153,7 @@ mkHsraInpTyInfo
-> InpObjFldMap
-> InpObjTyInfo
mkHsraInpTyInfo descM ty flds =
- InpObjTyInfo descM ty flds HasuraType
+ InpObjTyInfo descM ty flds TLHasuraType
mkHsraEnumTyInfo
:: Maybe G.Description
@@ -161,10 +161,10 @@ mkHsraEnumTyInfo
-> Map.HashMap G.EnumValue EnumValInfo
-> EnumTyInfo
mkHsraEnumTyInfo descM ty enumVals =
- EnumTyInfo descM ty enumVals HasuraType
+ EnumTyInfo descM ty enumVals TLHasuraType
mkHsraScalarTyInfo :: PGColType -> ScalarTyInfo
-mkHsraScalarTyInfo ty = ScalarTyInfo Nothing ty HasuraType
+mkHsraScalarTyInfo ty = ScalarTyInfo Nothing ty TLHasuraType
fromInpValL :: [InpValInfo] -> Map.HashMap G.Name InpValInfo
fromInpValL = mapFromL _iviName
@@ -211,7 +211,7 @@ mkCompExpInp colTy =
, bool [] (stDWithinGeoOpInpVal stDWithinGeographyInpTy :
map geoOpToInpVal geoOps) isGeographyType
, [InpValInfo Nothing "_is_null" Nothing $ G.TypeNamed (G.Nullability True) $ G.NamedType "Boolean"]
- ]) HasuraType
+ ]) TLHasuraType
where
tyDesc = mconcat
[ "expression to compare columns of type "
@@ -349,7 +349,7 @@ ordByEnumTy =
]
defaultTypes :: [TypeInfo]
-defaultTypes = $(fromSchemaDocQ defaultSchema HasuraType)
+defaultTypes = $(fromSchemaDocQ defaultSchema TLHasuraType)
mkGCtx :: TyAgg -> RootFlds -> InsCtxMap -> GCtx
@@ -397,10 +397,10 @@ mkGCtx tyAgg (RootFlds flds) insCtxMap =
-- _st_d_within has to stay with geometry type
stDWithinGeometryInpM =
- bool Nothing (Just $ stDWithinGeomInp) (PGGeometry `elem` colTys)
+ bool Nothing (Just stDWithinGeomInp) (PGGeometry `elem` colTys)
-- _st_d_within_geography is created for geography type
stDWithinGeographyInpM =
- bool Nothing (Just $ stDWithinGeogInp) (PGGeography `elem` colTys)
+ bool Nothing (Just stDWithinGeogInp) (PGGeography `elem` colTys)
stDWithinGeomInp =
mkHsraInpTyInfo Nothing stDWithinGeometryInpTy $ fromInpValL
@@ -417,3 +417,11 @@ mkGCtx tyAgg (RootFlds flds) insCtxMap =
emptyGCtx :: GCtx
emptyGCtx = mkGCtx mempty mempty mempty
+
+data RemoteGCtx
+ = RemoteGCtx
+ { _rgTypes :: !TypeMap
+ , _rgQueryRoot :: !ObjTyInfo
+ , _rgMutationRoot :: !(Maybe ObjTyInfo)
+ , _rgSubscriptionRoot :: !(Maybe ObjTyInfo)
+ } deriving (Show, Eq)
diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs
index 69ca1fc1d8b0c..94d77ab7d3bc7 100644
--- a/server/src-lib/Hasura/GraphQL/Execute.hs
+++ b/server/src-lib/Hasura/GraphQL/Execute.hs
@@ -69,7 +69,7 @@ assertSameLocationNodes
assertSameLocationNodes typeLocs =
case Set.toList (Set.fromList typeLocs) of
-- this shouldn't happen
- [] -> return VT.HasuraType
+ [] -> return VT.TLHasuraType
[loc] -> return loc
_ -> throw400 NotSupported msg
where
@@ -124,11 +124,11 @@ getExecPlanPartial userInfo sc enableAL req = do
typeLoc <- assertSameLocationNodes typeLocs
case typeLoc of
- VT.HasuraType -> do
+ VT.TLHasuraType -> do
rootSelSet <- runReaderT (VQ.validateGQ queryParts) gCtx
let varDefs = G._todVariableDefinitions $ VQ.qpOpDef queryParts
return $ GExPHasura (gCtx, rootSelSet, varDefs)
- VT.RemoteType _ rsi ->
+ VT.TLRemoteType _ rsi ->
return $ GExPRemote rsi opDef
where
role = userRole userInfo
diff --git a/server/src-lib/Hasura/GraphQL/RemoteServer.hs b/server/src-lib/Hasura/GraphQL/RemoteServer.hs
index 4065f9a155e0c..8e94af42a8ea4 100644
--- a/server/src-lib/Hasura/GraphQL/RemoteServer.hs
+++ b/server/src-lib/Hasura/GraphQL/RemoteServer.hs
@@ -22,12 +22,12 @@ import qualified Network.Wreq as Wreq
import Hasura.HTTP (wreqOptions)
import Hasura.RQL.DDL.Headers (getHeadersFromConf)
import Hasura.RQL.Types
+import Hasura.Server.Utils (bsToTxt, httpExceptToJSON)
+import qualified Hasura.GraphQL.Context as GC
import qualified Hasura.GraphQL.Schema as GS
import qualified Hasura.GraphQL.Validate.Types as VT
-
-
introspectionQuery :: BL.ByteString
introspectionQuery = $(embedStringFile "src-rsr/introspection.json")
@@ -36,61 +36,75 @@ fetchRemoteSchema
=> HTTP.Manager
-> RemoteSchemaName
-> RemoteSchemaInfo
- -> m GS.RemoteGCtx
+ -> m GC.RemoteGCtx
fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _) = do
headers <- getHeadersFromConf headerConf
- let hdrs = map (\(hn, hv) -> (CI.mk . T.encodeUtf8 $ hn, T.encodeUtf8 hv)) headers
+ let hdrs = flip map headers $
+ \(hn, hv) -> (CI.mk . T.encodeUtf8 $ hn, T.encodeUtf8 hv)
options = wreqOptions manager hdrs
res <- liftIO $ try $ Wreq.postWith options (show url) introspectionQuery
resp <- either throwHttpErr return res
let respData = resp ^. Wreq.responseBody
statusCode = resp ^. Wreq.responseStatus . Wreq.statusCode
- when (statusCode /= 200) $ schemaErr $ show respData
+ when (statusCode /= 200) $ throwNon200 statusCode respData
introspectRes :: (FromIntrospection IntrospectionResult) <-
- either schemaErr return $ J.eitherDecode respData
+ either (remoteSchemaErr . T.pack) return $ J.eitherDecode respData
let (sDoc, qRootN, mRootN, sRootN) =
fromIntrospection introspectRes
typMap <- either remoteSchemaErr return $ VT.fromSchemaDoc sDoc $
- VT.RemoteType name def
+ VT.TLRemoteType name def
let mQrTyp = Map.lookup qRootN typMap
- mMrTyp = maybe Nothing (\mr -> Map.lookup mr typMap) mRootN
- mSrTyp = maybe Nothing (\sr -> Map.lookup sr typMap) sRootN
+ mMrTyp = maybe Nothing (`Map.lookup` typMap) mRootN
+ mSrTyp = maybe Nothing (`Map.lookup` typMap) sRootN
qrTyp <- liftMaybe noQueryRoot mQrTyp
let mRmQR = VT.getObjTyM qrTyp
mRmMR = join $ VT.getObjTyM <$> mMrTyp
mRmSR = join $ VT.getObjTyM <$> mSrTyp
rmQR <- liftMaybe (err400 Unexpected "query root has to be an object type") mRmQR
- return $ GS.RemoteGCtx typMap rmQR mRmMR mRmSR
+ return $ GC.RemoteGCtx typMap rmQR mRmMR mRmSR
where
noQueryRoot = err400 Unexpected "query root not found in remote schema"
remoteSchemaErr :: (MonadError QErr m) => T.Text -> m a
remoteSchemaErr = throw400 RemoteSchemaError
- schemaErr err = remoteSchemaErr (T.pack err)
-
throwHttpErr :: (MonadError QErr m) => HTTP.HttpException -> m a
- throwHttpErr _ = schemaErr $
+ throwHttpErr = throwWithInternal httpExceptMsg . httpExceptToJSON
+
+ throwNon200 st = throwWithInternal (non200Msg st) . decodeNon200Resp
+
+ throwWithInternal msg v =
+ let err = err400 RemoteSchemaError $ T.pack msg
+ in throwError err{qeInternal = Just $ J.toJSON v}
+
+ httpExceptMsg =
"HTTP exception occurred while sending the request to " <> show url
+ non200Msg st = "introspection query to " <> show url
+ <> " has responded with " <> show st <> " status code"
+
+ decodeNon200Resp bs = case J.eitherDecode bs of
+ Right a -> J.object ["response" J..= (a :: J.Value)]
+ Left _ -> J.object ["raw_body" J..= bsToTxt (BL.toStrict bs)]
+
mergeSchemas
- :: (MonadIO m, MonadError QErr m)
+ :: (MonadError QErr m)
=> RemoteSchemaMap
-> GS.GCtxMap
- -> HTTP.Manager
- -> m (GS.GCtxMap, GS.GCtx) -- the merged GCtxMap and the default GCtx without roles
-mergeSchemas rmSchemaMap gCtxMap httpManager = do
- remoteSchemas <- forM (Map.toList rmSchemaMap) $ \(name, def) ->
- fetchRemoteSchema httpManager name def
+ -- the merged GCtxMap and the default GCtx without roles
+ -> m (GS.GCtxMap, GS.GCtx)
+mergeSchemas rmSchemaMap gCtxMap = do
def <- mkDefaultRemoteGCtx remoteSchemas
merged <- mergeRemoteSchema gCtxMap def
return (merged, def)
+ where
+ remoteSchemas = map rscGCtx $ Map.elems rmSchemaMap
mkDefaultRemoteGCtx
:: (MonadError QErr m)
- => [GS.RemoteGCtx] -> m GS.GCtx
+ => [GC.RemoteGCtx] -> m GS.GCtx
mkDefaultRemoteGCtx =
foldlM (\combG -> mergeGCtx combG . convRemoteGCtx) GS.emptyGCtx
@@ -126,12 +140,12 @@ mergeGCtx gCtx rmMergedGCtx = do
}
return updatedGCtx
-convRemoteGCtx :: GS.RemoteGCtx -> GS.GCtx
+convRemoteGCtx :: GC.RemoteGCtx -> GS.GCtx
convRemoteGCtx rmGCtx =
- GS.emptyGCtx { GS._gTypes = GS._rgTypes rmGCtx
- , GS._gQueryRoot = GS._rgQueryRoot rmGCtx
- , GS._gMutRoot = GS._rgMutationRoot rmGCtx
- , GS._gSubRoot = GS._rgSubscriptionRoot rmGCtx
+ GS.emptyGCtx { GS._gTypes = GC._rgTypes rmGCtx
+ , GS._gQueryRoot = GC._rgQueryRoot rmGCtx
+ , GS._gMutRoot = GC._rgMutationRoot rmGCtx
+ , GS._gSubRoot = GC._rgSubscriptionRoot rmGCtx
}
@@ -185,8 +199,8 @@ mergeTyMaps
-> VT.TypeMap
mergeTyMaps hTyMap rmTyMap newQR newMR =
let newTyMap = hTyMap <> rmTyMap
- newTyMap' = Map.insert (G.NamedType "query_root") (VT.TIObj newQR) $
- newTyMap
+ newTyMap' =
+ Map.insert (G.NamedType "query_root") (VT.TIObj newQR) newTyMap
in maybe newTyMap' (\mr -> Map.insert
(G.NamedType "mutation_root")
(VT.TIObj mr) newTyMap') newMR
@@ -280,28 +294,6 @@ instance J.FromJSON (FromIntrospection G.ValueConst) where
parseJSON = J.withText "defaultValue" $ \t -> fmap FromIntrospection
$ either (fail . T.unpack) return $ G.parseValueConst t
--- instance J.FromJSON (FromIntrospection G.ListType) where
--- parseJSON = parseJSON
-
--- instance (J.FromJSON (G.ObjectFieldG a)) =>
--- J.FromJSON (FromIntrospection (G.ObjectValueG a)) where
--- parseJSON = fmap (FromIntrospection . G.ObjectValueG) . J.parseJSON
-
--- instance (J.FromJSON a) => J.FromJSON (FromIntrospection (G.ObjectFieldG a)) where
--- parseJSON = J.withObject "ObjectValueG a" $ \o -> do
--- name <- o .: "name"
--- ofVal <- o .: "value"
--- return $ FromIntrospection $ G.ObjectFieldG name ofVal
-
--- instance J.FromJSON (FromIntrospection G.Value) where
--- parseJSON =
--- fmap FromIntrospection .
--- $(J.mkParseJSON J.defaultOptions{J.sumEncoding=J.UntaggedValue} ''G.Value)
-
-
--- $(J.deriveFromJSON J.defaultOptions{J.sumEncoding=J.UntaggedValue} ''G.Value)
-
-
instance J.FromJSON (FromIntrospection G.InterfaceTypeDefinition) where
parseJSON = J.withObject "InterfaceTypeDefinition" $ \o -> do
kind <- o .: "kind"
diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs
index 541ae1fd82b67..e2e84dd308db6 100644
--- a/server/src-lib/Hasura/GraphQL/Schema.hs
+++ b/server/src-lib/Hasura/GraphQL/Schema.hs
@@ -1,7 +1,7 @@
module Hasura.GraphQL.Schema
( mkGCtxMap
- , updateSCWithGCtx
, GCtxMap
+ , buildGCtxMapPG
, getGCtx
, GCtx(..)
, OpCtx(..)
@@ -11,7 +11,6 @@ module Hasura.GraphQL.Schema
, isAggFld
, qualObjectToName
-- Schema stitching related
- , RemoteGCtx (..)
, checkSchemaConflicts
, checkConflictingNode
, emptyGCtx
@@ -20,7 +19,6 @@ module Hasura.GraphQL.Schema
) where
-import Data.Has
import Data.Maybe (maybeToList)
import qualified Data.HashMap.Strict as Map
@@ -53,18 +51,6 @@ getTabInfo tc t =
onNothing (Map.lookup t tc) $
throw500 $ "table not found: " <>> t
-data RemoteGCtx
- = RemoteGCtx
- { _rgTypes :: !TypeMap
- , _rgQueryRoot :: !ObjTyInfo
- , _rgMutationRoot :: !(Maybe ObjTyInfo)
- , _rgSubscriptionRoot :: !(Maybe ObjTyInfo)
- } deriving (Show, Eq)
-
-instance Has TypeMap RemoteGCtx where
- getter = _rgTypes
- modifier f ctx = ctx { _rgTypes = f $ _rgTypes ctx }
-
type SelField = Either PGColInfo (RelInfo, Bool, AnnBoolExpPartialSQL, Maybe Int, Bool)
qualObjectToName :: (ToTxt a) => QualifiedObject a -> G.Name
@@ -265,7 +251,7 @@ mkTableObj
-> [SelField]
-> ObjTyInfo
mkTableObj tn allowedFlds =
- mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) HasuraType
+ mkObjTyInfo (Just desc) (mkTableTy tn) Set.empty (mapFromL _fiName flds) TLHasuraType
where
flds = concatMap (either (pure . mkPGColFld) mkRelFld') allowedFlds
mkRelFld' (relInfo, allowAgg, _, _, isNullable) =
@@ -1778,12 +1764,14 @@ mkGCtxMap tableCache functionCache = do
tableFltr ti = not (tiSystemDefined ti)
&& isValidObjectName (tiName ti)
-updateSCWithGCtx
- :: (MonadError QErr m)
- => SchemaCache -> m SchemaCache
-updateSCWithGCtx sc = do
+-- | build GraphQL schema from postgres tables and functions
+buildGCtxMapPG
+ :: (QErrM m, CacheRWM m)
+ => m ()
+buildGCtxMapPG = do
+ sc <- askSchemaCache
gCtxMap <- mkGCtxMap (scTables sc) (scFunctions sc)
- return $ sc {scGCtxMap = gCtxMap}
+ writeSchemaCache sc {scGCtxMap = gCtxMap}
getGCtx :: (CacheRM m) => RoleName -> GCtxMap -> m GCtx
getGCtx rn ctxMap = do
diff --git a/server/src-lib/Hasura/GraphQL/Validate/Types.hs b/server/src-lib/Hasura/GraphQL/Validate/Types.hs
index f504d97a21c8a..627b41ac49f63 100644
--- a/server/src-lib/Hasura/GraphQL/Validate/Types.hs
+++ b/server/src-lib/Hasura/GraphQL/Validate/Types.hs
@@ -126,8 +126,8 @@ type ParamMap = Map.HashMap G.Name InpValInfo
-- | location of the type: a hasura type or a remote type
data TypeLoc
- = HasuraType
- | RemoteType RemoteSchemaName RemoteSchemaInfo
+ = TLHasuraType
+ | TLRemoteType !RemoteSchemaName !RemoteSchemaInfo
deriving (Show, Eq, TH.Lift, Generic)
instance Hashable TypeLoc
diff --git a/server/src-lib/Hasura/RQL/DDL/Metadata.hs b/server/src-lib/Hasura/RQL/DDL/Metadata.hs
index 61c93b8702194..fdaf6acc3875c 100644
--- a/server/src-lib/Hasura/RQL/DDL/Metadata.hs
+++ b/server/src-lib/Hasura/RQL/DDL/Metadata.hs
@@ -41,7 +41,6 @@ import Hasura.RQL.Types
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
-import qualified Hasura.GraphQL.Schema as GS
import qualified Hasura.RQL.DDL.EventTrigger as DE
import qualified Hasura.RQL.DDL.Permission as DP
import qualified Hasura.RQL.DDL.Permission.Internal as DP
@@ -287,17 +286,10 @@ applyQP2 (ReplaceMetadata tables templates mFunctions mSchemas mCollections mAll
-- remote schemas
onJust mSchemas $ \schemas ->
withPathK "remote_schemas" $
- indexedForM_ schemas $ \conf ->
- void $ DRS.addRemoteSchemaP1 conf
- >>= DRS.addRemoteSchemaP2 conf
+ indexedMapM_ (void . DRS.addRemoteSchemaP2) schemas
- -- build GraphQL Context
- sc <- GS.updateSCWithGCtx =<< askSchemaCache
-
- -- resolve remote schemas
- httpMgr <- askHttpManager
- newSc <- DRS.resolveRemoteSchemas sc httpMgr
- writeSchemaCache newSc
+ -- build GraphQL Context with Remote schemas
+ DRS.buildGCtxMap
return successMsg
diff --git a/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs b/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs
index eaa9277e000d4..e51c1e9ce9004 100644
--- a/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs
+++ b/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs
@@ -1,13 +1,12 @@
module Hasura.RQL.DDL.RemoteSchema
( runAddRemoteSchema
- , addRemoteSchemaToCache
- , resolveRemoteSchemas
, runRemoveRemoteSchema
- , removeRemoteSchemaFromCache
, removeRemoteSchemaFromCatalog
- , refreshGCtxMapInSchema
+ , runReloadRemoteSchema
+ , buildGCtxMap
, fetchRemoteSchemas
, addRemoteSchemaP1
+ , addRemoteSchemaP2Setup
, addRemoteSchemaP2
) where
@@ -17,10 +16,10 @@ import Hasura.Prelude
import qualified Data.Aeson as J
import qualified Data.HashMap.Strict as Map
import qualified Database.PG.Query as Q
-import qualified Network.HTTP.Client as HTTP
import Hasura.GraphQL.RemoteServer
import Hasura.RQL.Types
+import Hasura.SQL.Types
import qualified Hasura.GraphQL.Schema as GS
@@ -31,20 +30,30 @@ runAddRemoteSchema
)
=> AddRemoteSchemaQuery -> m EncJSON
runAddRemoteSchema q = do
- addRemoteSchemaP1 q >>= addRemoteSchemaP2 q
+ addRemoteSchemaP1 name >> addRemoteSchemaP2 q
+ where
+ name = _arsqName q
addRemoteSchemaP1
- :: ( QErrM m, UserInfoM m
- , MonadIO m, HasHttpManager m
- )
- => AddRemoteSchemaQuery -> m RemoteSchemaInfo
-addRemoteSchemaP1 q = do
+ :: (QErrM m, UserInfoM m, CacheRM m)
+ => RemoteSchemaName -> m ()
+addRemoteSchemaP1 name = do
adminOnly
+ remoteSchemaMap <- scRemoteSchemas <$> askSchemaCache
+ onJust (Map.lookup name remoteSchemaMap) $ const $
+ throw400 AlreadyExists $ "remote schema with name "
+ <> name <<> " already exists"
+
+addRemoteSchemaP2Setup
+ :: (QErrM m, CacheRWM m, MonadIO m, HasHttpManager m)
+ => AddRemoteSchemaQuery -> m RemoteSchemaCtx
+addRemoteSchemaP2Setup q = do
httpMgr <- askHttpManager
rsi <- validateRemoteSchemaDef def
- -- TODO:- Maintain a cache of remote schema with it's GCtx
- void $ fetchRemoteSchema httpMgr name rsi
- return rsi
+ gCtx <- fetchRemoteSchema httpMgr name rsi
+ let rsCtx = RemoteSchemaCtx name gCtx rsi
+ addRemoteSchemaToCache rsCtx
+ return rsCtx
where
AddRemoteSchemaQuery name def _ = q
@@ -52,44 +61,19 @@ addRemoteSchemaP2
:: ( QErrM m
, CacheRWM m
, MonadTx m
+ , MonadIO m, HasHttpManager m
)
=> AddRemoteSchemaQuery
- -> RemoteSchemaInfo
-> m EncJSON
-addRemoteSchemaP2 q rsi = do
- addRemoteSchemaToCache name rsi
+addRemoteSchemaP2 q = do
+ void $ addRemoteSchemaP2Setup q
liftTx $ addRemoteSchemaToCatalog q
return successMsg
- where
- name = _arsqName q
-
-addRemoteSchemaToCache
- :: CacheRWM m
- => RemoteSchemaName
- -> RemoteSchemaInfo
- -> m ()
-addRemoteSchemaToCache name rmDef = do
- sc <- askSchemaCache
- let resolvers = scRemoteResolvers sc
- writeSchemaCache sc
- {scRemoteResolvers = Map.insert name rmDef resolvers}
-
-refreshGCtxMapInSchema
- :: (CacheRWM m, MonadIO m, MonadError QErr m, HasHttpManager m)
- => m ()
-refreshGCtxMapInSchema = do
- sc <- askSchemaCache
- gCtxMap <- GS.mkGCtxMap (scTables sc) (scFunctions sc)
- httpMgr <- askHttpManager
- (mergedGCtxMap, defGCtx) <-
- mergeSchemas (scRemoteResolvers sc) gCtxMap httpMgr
- writeSchemaCache sc { scGCtxMap = mergedGCtxMap
- , scDefaultRemoteGCtx = defGCtx }
runRemoveRemoteSchema
:: (QErrM m, UserInfoM m, CacheRWM m, MonadTx m)
- => RemoveRemoteSchemaQuery -> m EncJSON
-runRemoveRemoteSchema (RemoveRemoteSchemaQuery rsn)= do
+ => RemoteSchemaNameQuery -> m EncJSON
+runRemoveRemoteSchema (RemoteSchemaNameQuery rsn)= do
removeRemoteSchemaP1 rsn
removeRemoteSchemaP2 rsn
@@ -99,10 +83,9 @@ removeRemoteSchemaP1
removeRemoteSchemaP1 rsn = do
adminOnly
sc <- askSchemaCache
- let resolvers = scRemoteResolvers sc
- case Map.lookup rsn resolvers of
- Just _ -> return ()
- Nothing -> throw400 NotExists "no such remote schema"
+ let rmSchemas = scRemoteSchemas sc
+ void $ onNothing (Map.lookup rsn rmSchemas) $
+ throw400 NotExists "no such remote schema"
removeRemoteSchemaP2
:: ( CacheRWM m
@@ -111,30 +94,40 @@ removeRemoteSchemaP2
=> RemoteSchemaName
-> m EncJSON
removeRemoteSchemaP2 rsn = do
- removeRemoteSchemaFromCache rsn
+ delRemoteSchemaFromCache rsn
liftTx $ removeRemoteSchemaFromCatalog rsn
return successMsg
-removeRemoteSchemaFromCache
- :: CacheRWM m => RemoteSchemaName -> m ()
-removeRemoteSchemaFromCache rsn = do
- sc <- askSchemaCache
- let resolvers = scRemoteResolvers sc
- writeSchemaCache sc {scRemoteResolvers = Map.delete rsn resolvers}
-
-resolveRemoteSchemas
- :: ( MonadError QErr m
- , MonadIO m
+runReloadRemoteSchema
+ :: ( QErrM m, UserInfoM m , CacheRWM m
+ , MonadIO m, HasHttpManager m
)
- => SchemaCache -> HTTP.Manager -> m SchemaCache
-resolveRemoteSchemas sc httpMgr = do
- (mergedGCtxMap, defGCtx) <-
- mergeSchemas (scRemoteResolvers sc) gCtxMap httpMgr
- return $ sc { scGCtxMap = mergedGCtxMap
- , scDefaultRemoteGCtx = defGCtx
- }
- where
- gCtxMap = scGCtxMap sc
+ => RemoteSchemaNameQuery -> m EncJSON
+runReloadRemoteSchema (RemoteSchemaNameQuery name) = do
+ adminOnly
+ rmSchemas <- scRemoteSchemas <$> askSchemaCache
+ rsi <- fmap rscInfo $ onNothing (Map.lookup name rmSchemas) $
+ throw400 NotExists $ "remote schema with name "
+ <> name <<> " does not exist"
+ httpMgr <- askHttpManager
+ gCtx <- fetchRemoteSchema httpMgr name rsi
+ delRemoteSchemaFromCache name
+ addRemoteSchemaToCache $ RemoteSchemaCtx name gCtx rsi
+ return successMsg
+
+-- | build GraphQL schema
+buildGCtxMap
+ :: (QErrM m, CacheRWM m) => m ()
+buildGCtxMap = do
+ -- build GraphQL Context with Hasura schema
+ GS.buildGCtxMapPG
+ sc <- askSchemaCache
+ let gCtxMap = scGCtxMap sc
+ -- Stitch remote schemas
+ (mergedGCtxMap, defGCtx) <- mergeSchemas (scRemoteSchemas sc) gCtxMap
+ writeSchemaCache sc { scGCtxMap = mergedGCtxMap
+ , scDefaultRemoteGCtx = defGCtx
+ }
addRemoteSchemaToCatalog
:: AddRemoteSchemaQuery
diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs
index 84b28d9ea5cbf..7b8d98e62e4e4 100644
--- a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs
+++ b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs
@@ -65,7 +65,7 @@ trackExistingTableOrViewP1 (TrackTable vn) = do
throw400 AlreadyTracked $ "view/table already tracked : " <>> vn
trackExistingTableOrViewP2
- :: (QErrM m, CacheRWM m, MonadTx m, MonadIO m, HasHttpManager m)
+ :: (QErrM m, CacheRWM m, MonadTx m)
=> QualifiedTable -> Bool -> m EncJSON
trackExistingTableOrViewP2 vn isSystemDefined = do
sc <- askSchemaCache
@@ -79,9 +79,6 @@ trackExistingTableOrViewP2 vn isSystemDefined = do
_ -> throw500 $ "more than one row found for: " <>> vn
liftTx $ Q.catchE defaultTxErrorHandler $ saveTableToCatalog vn
- -- refresh the gCtx in schema cache
- refreshGCtxMapInSchema
-
return successMsg
where
QualifiedObject sn tn = vn
@@ -99,9 +96,7 @@ trackExistingTableOrViewP2 vn isSystemDefined = do
|] (sn, tn) True
runTrackTableQ
- :: ( QErrM m, CacheRWM m, MonadTx m
- , MonadIO m, HasHttpManager m, UserInfoM m
- )
+ :: (QErrM m, CacheRWM m, MonadTx m, UserInfoM m)
=> TrackTable -> m EncJSON
runTrackTableQ q = do
trackExistingTableOrViewP1 q
@@ -257,7 +252,7 @@ unTrackExistingTableOrViewP1 (UntrackTable vn _) = do
"view/table already untracked : " <>> vn
unTrackExistingTableOrViewP2
- :: (QErrM m, CacheRWM m, MonadTx m, MonadIO m, HasHttpManager m)
+ :: (QErrM m, CacheRWM m, MonadTx m)
=> UntrackTable -> m EncJSON
unTrackExistingTableOrViewP2 (UntrackTable qtn cascade) = do
sc <- askSchemaCache
@@ -275,9 +270,6 @@ unTrackExistingTableOrViewP2 (UntrackTable qtn cascade) = do
-- delete the table and its direct dependencies
delTableAndDirectDeps qtn
- -- refresh the gctxmap in schema cache
- refreshGCtxMapInSchema
-
return successMsg
where
isDirectDep = \case
@@ -285,9 +277,7 @@ unTrackExistingTableOrViewP2 (UntrackTable qtn cascade) = do
_ -> False
runUntrackTableQ
- :: ( QErrM m, CacheRWM m, MonadTx m
- , MonadIO m, HasHttpManager m, UserInfoM m
- )
+ :: (QErrM m, CacheRWM m, MonadTx m, UserInfoM m)
=> UntrackTable -> m EncJSON
runUntrackTableQ q = do
unTrackExistingTableOrViewP1 q
@@ -349,7 +339,6 @@ buildSchemaCacheG withSetup = do
when withSetup $ liftTx $ Q.catchE defaultTxErrorHandler clearHdbViews
-- reset the current schemacache
writeSchemaCache emptySchemaCache
- hMgr <- askHttpManager
sqlGenCtx <- askSQLGenCtx
-- fetch all catalog metadata
@@ -442,12 +431,11 @@ buildSchemaCacheG withSetup = do
-- allow list
replaceAllowlist $ concatMap _cdQueries allowlistDefs
- -- build GraphQL context
- postGCtxSc <- askSchemaCache >>= GS.updateSCWithGCtx
- writeSchemaCache postGCtxSc
+ -- build GraphQL context with tables and functions
+ GS.buildGCtxMapPG
-- remote schemas
- forM_ remoteSchemas $ resolveSingleRemoteSchema hMgr
+ forM_ remoteSchemas resolveSingleRemoteSchema
where
permHelper setup sqlGenCtx qt rn pDef pa = do
@@ -460,17 +448,16 @@ buildSchemaCacheG withSetup = do
addPermToCache qt rn pa permInfo deps
-- p2F qt rn p1Res
- resolveSingleRemoteSchema hMgr rs = do
- let AddRemoteSchemaQuery name def _ = rs
+ resolveSingleRemoteSchema rs = do
+ let AddRemoteSchemaQuery name _ _ = rs
mkInconsObj = InconsistentMetadataObj (MORemoteSchema name)
MOTRemoteSchema (toJSON rs)
handleInconsistentObj mkInconsObj $ do
- rsi <- validateRemoteSchemaDef def
- addRemoteSchemaToCache name rsi
+ rsCtx <- addRemoteSchemaP2Setup rs
sc <- askSchemaCache
let gCtxMap = scGCtxMap sc
defGCtx = scDefaultRemoteGCtx sc
- rGCtx <- convRemoteGCtx <$> fetchRemoteSchema hMgr name rsi
+ rGCtx = convRemoteGCtx $ rscGCtx rsCtx
mergedGCtxMap <- mergeRemoteSchema gCtxMap rGCtx
mergedDefGCtx <- mergeGCtx defGCtx rGCtx
writeSchemaCache sc { scGCtxMap = mergedGCtxMap
@@ -599,9 +586,6 @@ execWithMDCheck (RunSQL t cascade _) = do
bool withoutReload withReload reloadRequired
- -- refresh the gCtxMap in schema cache
- refreshGCtxMapInSchema
-
return res
where
reportFuncs = T.intercalate ", " . map dquoteTxt
diff --git a/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs b/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs
index 56189d0a83e8d..dc2f4d4c6cde6 100644
--- a/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs
+++ b/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs
@@ -7,20 +7,22 @@ import System.Environment (lookupEnv)
import qualified Data.Aeson as J
import qualified Data.Aeson.Casing as J
import qualified Data.Aeson.TH as J
-import qualified Data.HashMap.Strict as Map
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import qualified Network.URI.Extended as N
import Hasura.RQL.DDL.Headers (HeaderConf (..))
import Hasura.RQL.Types.Error
+import Hasura.SQL.Types (DQuote)
type UrlFromEnv = Text
newtype RemoteSchemaName
= RemoteSchemaName
{ unRemoteSchemaName :: Text}
- deriving (Show, Eq, Lift, Hashable, J.ToJSON, J.ToJSONKey, J.FromJSON, Q.ToPrepArg, Q.FromCol)
+ deriving ( Show, Eq, Lift, Hashable, J.ToJSON, J.ToJSONKey
+ , J.FromJSON, Q.ToPrepArg, Q.FromCol, DQuote
+ )
data RemoteSchemaInfo
= RemoteSchemaInfo
@@ -43,25 +45,6 @@ data RemoteSchemaDef
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase) ''RemoteSchemaDef)
-type RemoteSchemaMap = Map.HashMap RemoteSchemaName RemoteSchemaInfo
-
--- instance J.ToJSON RemoteSchemaDef where
--- toJSON (RemoteSchemaDef name eUrlVal headers fwdHdrs) =
--- case eUrlVal of
--- Left url ->
--- J.object [ "url" J..= url
--- , "headers" J..= headers
--- , "name" J..= name
--- , "forward_client_headers" J..= fwdHdrs
--- ]
--- Right urlFromEnv ->
--- J.object [ "url_from_env" J..= urlFromEnv
--- , "headers" J..= headers
--- , "name" J..= name
--- , "forward_client_headers" J..= fwdHdrs
--- ]
-
-
data AddRemoteSchemaQuery
= AddRemoteSchemaQuery
{ _arsqName :: !RemoteSchemaName -- TODO: name validation: cannot be empty?
@@ -71,20 +54,12 @@ data AddRemoteSchemaQuery
$(J.deriveJSON (J.aesonDrop 5 J.snakeCase) ''AddRemoteSchemaQuery)
--- data AddRemoteSchemaQuery'
--- = AddRemoteSchemaQuery'
--- { _arsqUrl :: !(Maybe N.URI)
--- , _arsqUrlFromEnv :: !(Maybe Text)
--- , _arsqHeaders :: !(Maybe [HeaderConf])
--- , _arsqForwardClientHeaders :: !Bool
--- } deriving (Show, Eq, Lift)
-
-data RemoveRemoteSchemaQuery
- = RemoveRemoteSchemaQuery
- { _rrsqName :: !RemoteSchemaName
+newtype RemoteSchemaNameQuery
+ = RemoteSchemaNameQuery
+ { _rsnqName :: RemoteSchemaName
} deriving (Show, Eq, Lift)
-$(J.deriveJSON (J.aesonDrop 5 J.snakeCase) ''RemoveRemoteSchemaQuery)
+$(J.deriveJSON (J.aesonDrop 5 J.snakeCase) ''RemoteSchemaNameQuery)
getUrlFromEnv :: (MonadIO m, MonadError QErr m) => Text -> m N.URI
getUrlFromEnv urlFromEnv = do
diff --git a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
index 9abf5a8dae47e..ae3d7b6c3ef29 100644
--- a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
+++ b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
@@ -24,6 +24,11 @@ module Hasura.RQL.Types.SchemaCache
, modTableInCache
, delTableFromCache
+ , RemoteSchemaCtx(..)
+ , RemoteSchemaMap
+ , addRemoteSchemaToCache
+ , delRemoteSchemaFromCache
+
, WithDeps
, CacheRM(..)
@@ -103,6 +108,7 @@ module Hasura.RQL.Types.SchemaCache
) where
import qualified Hasura.GraphQL.Context as GC
+
import Hasura.Prelude
import Hasura.RQL.Types.BoolExp
import Hasura.RQL.Types.Common
@@ -410,6 +416,18 @@ $(deriveToJSON (aesonDrop 2 snakeCase) ''FunctionInfo)
type TableCache = M.HashMap QualifiedTable TableInfo -- info of all tables
type FunctionCache = M.HashMap QualifiedFunction FunctionInfo -- info of all functions
+data RemoteSchemaCtx
+ = RemoteSchemaCtx
+ { rscName :: !RemoteSchemaName
+ , rscGCtx :: !GC.RemoteGCtx
+ , rscInfo :: !RemoteSchemaInfo
+ } deriving (Show, Eq)
+
+instance ToJSON RemoteSchemaCtx where
+ toJSON = toJSON . rscInfo
+
+type RemoteSchemaMap = M.HashMap RemoteSchemaName RemoteSchemaCtx
+
type DepMap = M.HashMap SchemaObjId (HS.HashSet SchemaDependency)
addToDepMap :: SchemaObjId -> [SchemaDependency] -> DepMap -> DepMap
@@ -443,8 +461,8 @@ data SchemaCache
{ scTables :: !TableCache
, scFunctions :: !FunctionCache
, scQTemplates :: !QTemplateCache
+ , scRemoteSchemas :: !RemoteSchemaMap
, scAllowlist :: !(HS.HashSet GQLQuery)
- , scRemoteResolvers :: !RemoteSchemaMap
, scGCtxMap :: !GC.GCtxMap
, scDefaultRemoteGCtx :: !GC.GCtx
, scDepMap :: !DepMap
@@ -513,8 +531,8 @@ delQTemplateFromCache qtn = do
emptySchemaCache :: SchemaCache
emptySchemaCache =
- SchemaCache M.empty M.empty M.empty HS.empty
- M.empty M.empty GC.emptyGCtx mempty []
+ SchemaCache M.empty M.empty M.empty M.empty
+ HS.empty M.empty GC.emptyGCtx mempty []
modTableCache :: (CacheRWM m) => TableCache -> m ()
modTableCache tc = do
@@ -789,6 +807,32 @@ data TemplateParamInfo
, tpiDefault :: !(Maybe Value)
} deriving (Show, Eq)
+addRemoteSchemaToCache
+ :: (QErrM m, CacheRWM m) => RemoteSchemaCtx -> m ()
+addRemoteSchemaToCache rmCtx = do
+ sc <- askSchemaCache
+ let rmSchemas = scRemoteSchemas sc
+ name = rscName rmCtx
+ -- ideally, remote schema shouldn't present in cache
+ -- if present unexpected 500 is thrown
+ onJust (M.lookup name rmSchemas) $ const $
+ throw500 $ "remote schema with name " <> name
+ <<> " already found in cache"
+ writeSchemaCache sc
+ {scRemoteSchemas = M.insert name rmCtx rmSchemas}
+
+delRemoteSchemaFromCache
+ :: (QErrM m, CacheRWM m) => RemoteSchemaName -> m ()
+delRemoteSchemaFromCache name = do
+ sc <- askSchemaCache
+ let rmSchemas = scRemoteSchemas sc
+ -- ideally, remote schema should be present in cache
+ -- if not present unexpected 500 is thrown
+ void $ onNothing (M.lookup name rmSchemas) $
+ throw500 $ "remote schema with name " <> name
+ <<> " not found in cache"
+ writeSchemaCache sc {scRemoteSchemas = M.delete name rmSchemas}
+
replaceAllowlist
:: (CacheRWM m)
=> QueryList -> m ()
diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs
index c13ff3f69977b..6d3db6e2b9017 100644
--- a/server/src-lib/Hasura/Server/App.hs
+++ b/server/src-lib/Hasura/Server/App.hs
@@ -35,7 +35,6 @@ import qualified Database.PG.Query as Q
import qualified Hasura.GraphQL.Execute as E
import qualified Hasura.GraphQL.Execute.LiveQuery as EL
import qualified Hasura.GraphQL.Explain as GE
-import qualified Hasura.GraphQL.Schema as GS
import qualified Hasura.GraphQL.Transport.HTTP as GH
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
import qualified Hasura.GraphQL.Transport.WebSocket as WS
@@ -43,7 +42,6 @@ import qualified Hasura.Logging as L
import Hasura.EncJSON
import Hasura.Prelude hiding (get, put)
-import Hasura.RQL.DDL.RemoteSchema
import Hasura.RQL.DDL.Schema.Table
import Hasura.RQL.DML.QueryTemplate
import Hasura.RQL.Types
@@ -188,7 +186,7 @@ logResult
-> Either QErr BL.ByteString -> Maybe (UTCTime, UTCTime)
-> m ()
logResult userInfoM req reqBody logger res qTime =
- liftIO $ (L.unLogger logger) $ mkAccessLog userInfoM req (reqBody, res) qTime
+ liftIO $ L.unLogger logger $ mkAccessLog userInfoM req (reqBody, res) qTime
logError
:: MonadIO m
@@ -254,7 +252,7 @@ v1QueryHandler :: RQLQuery -> Handler (HttpResponse EncJSON)
v1QueryHandler query = do
scRef <- scCacheRef . hcServerCtx <$> ask
logger <- scLogger . hcServerCtx <$> ask
- res <- bool (fst <$> dbAction) (withSCUpdate scRef logger dbActionReload) $
+ res <- bool (fst <$> dbAction) (withSCUpdate scRef logger dbAction) $
queryNeedsReload query
return $ HttpResponse res Nothing
where
@@ -269,14 +267,6 @@ v1QueryHandler query = do
instanceId <- scInstanceId . hcServerCtx <$> ask
runQuery pgExecCtx instanceId userInfo schemaCache httpMgr sqlGenCtx query
- -- Also update the schema cache
- dbActionReload = do
- (resp, newSc) <- dbAction
- httpMgr <- scManager . hcServerCtx <$> ask
- --FIXME: should we be fetching the remote schema again? if not how do we get the remote schema?
- newSc' <- GS.updateSCWithGCtx newSc >>= flip resolveRemoteSchemas httpMgr
- return (resp, newSc')
-
v1Alpha1GQHandler :: GH.GQLReqUnparsed -> Handler (HttpResponse EncJSON)
v1Alpha1GQHandler query = do
userInfo <- asks hcUser
@@ -335,7 +325,7 @@ consoleAssetsHandler logger dir path = do
headers = ("Content-Type", mimeType) : encHeader
mkConsoleHTML :: T.Text -> AuthMode -> Bool -> Maybe Text -> Either String T.Text
-mkConsoleHTML path authMode enableTelemetry consoleAssetsDir = do
+mkConsoleHTML path authMode enableTelemetry consoleAssetsDir =
bool (Left errMsg) (Right res) $ null errs
where
(errs, res) = M.checkedSubstitute consoleTmplt $
@@ -451,8 +441,7 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do
middleware $ corsMiddleware (mkDefaultCorsPolicy corsCfg)
-- API Console and Root Dir
- when (enableConsole && enableMetadata) $ do
- serveApiConsole
+ when (enableConsole && enableMetadata) serveApiConsole
-- Health check endpoint
get "healthz" $ do
diff --git a/server/src-lib/Hasura/Server/Query.hs b/server/src-lib/Hasura/Server/Query.hs
index 59671d31a2663..c7f6e9c321b35 100644
--- a/server/src-lib/Hasura/Server/Query.hs
+++ b/server/src-lib/Hasura/Server/Query.hs
@@ -68,7 +68,8 @@ data RQLQuery
-- schema-stitching, custom resolver related
| RQAddRemoteSchema !AddRemoteSchemaQuery
- | RQRemoveRemoteSchema !RemoveRemoteSchemaQuery
+ | RQRemoveRemoteSchema !RemoteSchemaNameQuery
+ | RQReloadRemoteSchema !RemoteSchemaNameQuery
| RQCreateEventTrigger !CreateEventTriggerQuery
| RQDeleteEventTrigger !DeleteEventTriggerQuery
@@ -214,6 +215,7 @@ queryNeedsReload qi = case qi of
RQAddRemoteSchema _ -> True
RQRemoveRemoteSchema _ -> True
+ RQReloadRemoteSchema _ -> True
RQCreateEventTrigger _ -> True
RQDeleteEventTrigger _ -> True
@@ -249,67 +251,73 @@ runQueryM
)
=> RQLQuery
-> m EncJSON
-runQueryM rq = withPathK "args" $ case rq of
- RQAddExistingTableOrView q -> runTrackTableQ q
- RQTrackTable q -> runTrackTableQ q
- RQUntrackTable q -> runUntrackTableQ q
-
- RQTrackFunction q -> runTrackFunc q
- RQUntrackFunction q -> runUntrackFunc q
-
- RQCreateObjectRelationship q -> runCreateObjRel q
- RQCreateArrayRelationship q -> runCreateArrRel q
- RQDropRelationship q -> runDropRel q
- RQSetRelationshipComment q -> runSetRelComment q
- RQRenameRelationship q -> runRenameRel q
-
- RQCreateInsertPermission q -> runCreatePerm q
- RQCreateSelectPermission q -> runCreatePerm q
- RQCreateUpdatePermission q -> runCreatePerm q
- RQCreateDeletePermission q -> runCreatePerm q
-
- RQDropInsertPermission q -> runDropPerm q
- RQDropSelectPermission q -> runDropPerm q
- RQDropUpdatePermission q -> runDropPerm q
- RQDropDeletePermission q -> runDropPerm q
- RQSetPermissionComment q -> runSetPermComment q
-
- RQGetInconsistentMetadata q -> runGetInconsistentMetadata q
- RQDropInconsistentMetadata q -> runDropInconsistentMetadata q
-
- RQInsert q -> runInsert q
- RQSelect q -> runSelect q
- RQUpdate q -> runUpdate q
- RQDelete q -> runDelete q
- RQCount q -> runCount q
-
- RQAddRemoteSchema q -> runAddRemoteSchema q
- RQRemoveRemoteSchema q -> runRemoveRemoteSchema q
-
- RQCreateEventTrigger q -> runCreateEventTriggerQuery q
- RQDeleteEventTrigger q -> runDeleteEventTriggerQuery q
- RQRedeliverEvent q -> runRedeliverEvent q
- RQInvokeEventTrigger q -> runInvokeEventTrigger q
-
- RQCreateQueryTemplate q -> runCreateQueryTemplate q
- RQDropQueryTemplate q -> runDropQueryTemplate q
- RQExecuteQueryTemplate q -> runExecQueryTemplate q
- RQSetQueryTemplateComment q -> runSetQueryTemplateComment q
-
- RQCreateQueryCollection q -> runCreateCollection q
- RQDropQueryCollection q -> runDropCollection q
- RQAddQueryToCollection q -> runAddQueryToCollection q
- RQDropQueryFromCollection q -> runDropQueryFromCollection q
- RQAddCollectionToAllowlist q -> runAddCollectionToAllowlist q
- RQDropCollectionFromAllowlist q -> runDropCollectionFromAllowlist q
-
- RQReplaceMetadata q -> runReplaceMetadata q
- RQClearMetadata q -> runClearMetadata q
- RQExportMetadata q -> runExportMetadata q
- RQReloadMetadata q -> runReloadMetadata q
-
- RQDumpInternalState q -> runDumpInternalState q
-
- RQRunSql q -> runRunSQL q
-
- RQBulk qs -> encJFromList <$> indexedMapM runQueryM qs
+runQueryM rq =
+ withPathK "args" $ runQueryM' <* rebuildGCtx
+ where
+ rebuildGCtx = when (queryNeedsReload rq) buildGCtxMap
+
+ runQueryM' = case rq of
+ RQAddExistingTableOrView q -> runTrackTableQ q
+ RQTrackTable q -> runTrackTableQ q
+ RQUntrackTable q -> runUntrackTableQ q
+
+ RQTrackFunction q -> runTrackFunc q
+ RQUntrackFunction q -> runUntrackFunc q
+
+ RQCreateObjectRelationship q -> runCreateObjRel q
+ RQCreateArrayRelationship q -> runCreateArrRel q
+ RQDropRelationship q -> runDropRel q
+ RQSetRelationshipComment q -> runSetRelComment q
+ RQRenameRelationship q -> runRenameRel q
+
+ RQCreateInsertPermission q -> runCreatePerm q
+ RQCreateSelectPermission q -> runCreatePerm q
+ RQCreateUpdatePermission q -> runCreatePerm q
+ RQCreateDeletePermission q -> runCreatePerm q
+
+ RQDropInsertPermission q -> runDropPerm q
+ RQDropSelectPermission q -> runDropPerm q
+ RQDropUpdatePermission q -> runDropPerm q
+ RQDropDeletePermission q -> runDropPerm q
+ RQSetPermissionComment q -> runSetPermComment q
+
+ RQGetInconsistentMetadata q -> runGetInconsistentMetadata q
+ RQDropInconsistentMetadata q -> runDropInconsistentMetadata q
+
+ RQInsert q -> runInsert q
+ RQSelect q -> runSelect q
+ RQUpdate q -> runUpdate q
+ RQDelete q -> runDelete q
+ RQCount q -> runCount q
+
+ RQAddRemoteSchema q -> runAddRemoteSchema q
+ RQRemoveRemoteSchema q -> runRemoveRemoteSchema q
+ RQReloadRemoteSchema q -> runReloadRemoteSchema q
+
+ RQCreateEventTrigger q -> runCreateEventTriggerQuery q
+ RQDeleteEventTrigger q -> runDeleteEventTriggerQuery q
+ RQRedeliverEvent q -> runRedeliverEvent q
+ RQInvokeEventTrigger q -> runInvokeEventTrigger q
+
+ RQCreateQueryTemplate q -> runCreateQueryTemplate q
+ RQDropQueryTemplate q -> runDropQueryTemplate q
+ RQExecuteQueryTemplate q -> runExecQueryTemplate q
+ RQSetQueryTemplateComment q -> runSetQueryTemplateComment q
+
+ RQCreateQueryCollection q -> runCreateCollection q
+ RQDropQueryCollection q -> runDropCollection q
+ RQAddQueryToCollection q -> runAddQueryToCollection q
+ RQDropQueryFromCollection q -> runDropQueryFromCollection q
+ RQAddCollectionToAllowlist q -> runAddCollectionToAllowlist q
+ RQDropCollectionFromAllowlist q -> runDropCollectionFromAllowlist q
+
+ RQReplaceMetadata q -> runReplaceMetadata q
+ RQClearMetadata q -> runClearMetadata q
+ RQExportMetadata q -> runExportMetadata q
+ RQReloadMetadata q -> runReloadMetadata q
+
+ RQDumpInternalState q -> runDumpInternalState q
+
+ RQRunSql q -> runRunSQL q
+
+ RQBulk qs -> encJFromList <$> indexedMapM runQueryM qs
diff --git a/server/src-lib/Hasura/Server/Telemetry.hs b/server/src-lib/Hasura/Server/Telemetry.hs
index 1e57a57896b4b..4c7f615c02b41 100644
--- a/server/src-lib/Hasura/Server/Telemetry.hs
+++ b/server/src-lib/Hasura/Server/Telemetry.hs
@@ -145,7 +145,7 @@ computeMetrics sc =
PermissionMetric selPerms insPerms updPerms delPerms nRoles
evtTriggers = Map.size $ Map.filter (not . Map.null)
$ Map.map tiEventTriggerInfoMap usrTbls
- rmSchemas = Map.size $ scRemoteResolvers sc
+ rmSchemas = Map.size $ scRemoteSchemas sc
funcs = Map.size $ Map.filter (not . fiSystemDefined) $ scFunctions sc
in Metrics nTables nViews relMetrics permMetrics evtTriggers rmSchemas funcs
diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs
index bf52d2125683d..a7254a16fbd01 100644
--- a/server/src-lib/Hasura/Server/Utils.hs
+++ b/server/src-lib/Hasura/Server/Utils.hs
@@ -17,6 +17,7 @@ import qualified Data.Text.Encoding as TE
import qualified Data.Text.Encoding.Error as TE
import qualified Data.Text.IO as TI
import qualified Language.Haskell.TH.Syntax as TH
+import qualified Network.HTTP.Client as HC
import qualified Network.HTTP.Types as HTTP
import qualified Text.Ginger as TG
import qualified Text.Regex.TDFA as TDFA
@@ -166,8 +167,29 @@ diffTimeToMicro diff =
where
aSecond = 1000 * 1000
--- ignore the following request headers from the client
+-- json representation of HTTP exception
+httpExceptToJSON :: HC.HttpException -> Value
+httpExceptToJSON e = case e of
+ HC.HttpExceptionRequest x c ->
+ let reqObj = object
+ [ "host" .= bsToTxt (HC.host x)
+ , "port" .= show (HC.port x)
+ , "secure" .= HC.secure x
+ , "path" .= bsToTxt (HC.path x)
+ , "method" .= bsToTxt (HC.method x)
+ , "proxy" .= (showProxy <$> HC.proxy x)
+ , "redirectCount" .= show (HC.redirectCount x)
+ , "responseTimeout" .= show (HC.responseTimeout x)
+ , "requestVersion" .= show (HC.requestVersion x)
+ ]
+ msg = show c
+ in object ["request" .= reqObj, "message" .= msg]
+ _ -> toJSON $ show e
+ where
+ showProxy (HC.Proxy h p) =
+ "host: " <> bsToTxt h <> " port: " <> T.pack (show p)
+-- ignore the following request headers from the client
commonClientHeadersIgnored :: (IsString a) => [a]
commonClientHeadersIgnored =
[ "Content-Length", "Content-MD5", "User-Agent", "Host"
diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py
index b3ef7c22e3021..f43c332960f5b 100644
--- a/server/tests-py/test_schema_stitching.py
+++ b/server/tests-py/test_schema_stitching.py
@@ -34,6 +34,14 @@ def mk_delete_remote_q(name):
}
}
+def mk_reload_remote_q(name):
+ return {
+ "type" : "reload_remote_schema",
+ "args" : {
+ "name" : name
+ }
+ }
+
class TestRemoteSchemaBasic:
""" basic => no hasura tables are tracked """
@@ -82,12 +90,25 @@ def test_add_schema_conflicts(self, hge_ctx):
assert st_code == 400
assert resp['code'] == 'remote-schema-conflicts'
+ def test_remove_schema_error(self, hge_ctx):
+ """remove remote schema which is not added"""
+ q = mk_delete_remote_q('random name')
+ st_code, resp = hge_ctx.v1q(q)
+ assert st_code == 400
+ assert resp['code'] == 'not-exists'
+
+ def test_reload_remote_schema(self, hge_ctx):
+ """reload a remote schema"""
+ q = mk_reload_remote_q('simple 1')
+ st_code, resp = hge_ctx.v1q(q)
+ assert st_code == 200
+
def test_add_second_remote_schema(self, hge_ctx):
"""add 2 remote schemas with different node and types"""
q = mk_add_remote_q('my remote', 'http://localhost:5000/user-graphql')
st_code, resp = hge_ctx.v1q(q)
assert st_code == 200, resp
- hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote"}})
+ st_code, resp = hge_ctx.v1q(mk_delete_remote_q('my remote'))
assert st_code == 200, resp
def test_add_remote_schema_with_interfaces(self, hge_ctx):
@@ -96,8 +117,7 @@ def test_add_remote_schema_with_interfaces(self, hge_ctx):
st_code, resp = hge_ctx.v1q(q)
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir + '/character_interface_query.yaml')
- hge_ctx.v1q({"type": "remove_remote_schema",
- "args": {"name": "my remote interface one"}})
+ st_code, resp = hge_ctx.v1q(mk_delete_remote_q('my remote interface one'))
assert st_code == 200, resp
def test_add_remote_schema_with_interface_err_empty_fields_list(self, hge_ctx):
@@ -219,8 +239,8 @@ def test_introspection(self, hge_ctx):
def test_add_schema_duplicate_name(self, hge_ctx):
q = mk_add_remote_q('simple2-graphql', 'http://localhost:5000/country-graphql')
st_code, resp = hge_ctx.v1q(q)
- assert st_code == 500, resp
- assert resp['code'] == 'unexpected'
+ assert st_code == 400, resp
+ assert resp['code'] == 'already-exists'
def test_add_schema_same_type_containing_same_scalar(self, hge_ctx):
"""