mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
server/postgres: Support scalar computed fields in remote joins
https://github.com/hasura/graphql-engine-mono/pull/1692 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> GitOrigin-RevId: fcef85910899859f7421cad554c022f8023965ea
This commit is contained in:
parent
effb221d97
commit
a375f8c105
@ -3,6 +3,7 @@
|
||||
## Next release
|
||||
(Add entries below in the order of server, console, cli, docs, others)
|
||||
|
||||
- server: support scalar computed fields in remote joins (close #7101)
|
||||
- server: Support computed fields in query filter (`where` argument) (close #7100)
|
||||
- server: add a `$.detail.operation.request_mode` field to `http-log` which takes the values `"single"` or `"batched"` to log whether a GraphQL request was executed on its own or as part of a batch
|
||||
- server: add `query` field to `http-log` and `websocket-log` in non-error cases
|
||||
|
@ -72,8 +72,8 @@ Args syntax
|
||||
- Object with table name and schema
|
||||
* - hasura_fields
|
||||
- true
|
||||
- [:ref:`PGColumn <PGColumn>`]
|
||||
- Column(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
|
||||
- [:ref:`PGColumn <PGColumn>` | :ref:`ComputedFieldName <ComputedFieldName>`]
|
||||
- Column/Computed field(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
|
||||
* - remote_schema
|
||||
- true
|
||||
- :ref:`RemoteSchemaName <RemoteSchemaName>`
|
||||
@ -192,4 +192,4 @@ Args syntax
|
||||
* - name
|
||||
- true
|
||||
- :ref:`RemoteRelationshipName`
|
||||
- Name of the remote relationship
|
||||
- Name of the remote relationship
|
||||
|
@ -74,8 +74,8 @@ Args syntax
|
||||
- Object with table name and schema
|
||||
* - hasura_fields
|
||||
- true
|
||||
- [:ref:`PGColumn <PGColumn>`]
|
||||
- Column(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
|
||||
- [:ref:`PGColumn <PGColumn>` | :ref:`ComputedFieldName <ComputedFieldName>`]
|
||||
- Column/Computed field(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
|
||||
* - remote_schema
|
||||
- true
|
||||
- :ref:`RemoteSchemaName <RemoteSchemaName>`
|
||||
@ -194,4 +194,4 @@ Args syntax
|
||||
* - name
|
||||
- true
|
||||
- :ref:`RemoteRelationshipName`
|
||||
- Name of the remote relationship
|
||||
- Name of the remote relationship
|
||||
|
@ -57,7 +57,7 @@ Computed fields whose associated SQL function returns a
|
||||
Let's say we have the following schema:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
|
||||
authors(id integer, first_name text, last_name text)
|
||||
|
||||
:ref:`Define an SQL function <create_sql_functions>` called ``author_full_name``:
|
||||
@ -109,9 +109,9 @@ The return table must be tracked to define such a computed field.
|
||||
Let's say we have the following schema:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
|
||||
authors(id integer, first_name text, last_name text)
|
||||
|
||||
|
||||
articles(id integer, title text, content text, author_id integer)
|
||||
|
||||
Now we can define a :ref:`table relationship <table_relationships>` on the ``authors``
|
||||
@ -345,3 +345,77 @@ The value of generated columns is also computed from other columns of a table. P
|
||||
come with their own limitations. Hasura's computed fields are defined via an SQL function, which allows users
|
||||
to define any complex business logic in a function. Generated columns will go together with computed fields where
|
||||
Hasura treats generated columns as normal Postgres columns.
|
||||
|
||||
Computed fields in Remote relationships
|
||||
---------------------------------------
|
||||
|
||||
Using computed fields in :doc:`Remote relationships <remote-relationships/index>` allows transformation of data
|
||||
from table columns before joining with data from remote sources. For instance, suppose we want to extract certain
|
||||
field from a ``json`` column and join it with a field in a remote schema by argument value. We would define a computed
|
||||
field which returns a scalar type of the field value in the ``json`` column and use it to join the graphql field of
|
||||
the remote schema. Consider the following Postgres schema.
|
||||
|
||||
.. thumbnail:: /img/graphql/core/databases/postgres/schema/computed-fields-remote-relationship.png
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE TABLE "user" (id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, address json NOT NULL);
|
||||
|
||||
-- SQL function returns city of a "user" using "->>" json operator
|
||||
CREATE FUNCTION get_city(table_row "user")
|
||||
RETURNS TEXT AS $$
|
||||
SELECT table_row.address ->> 'city'
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
Now, let's track the table and add computed field ``user_city`` using the SQL function ``get_city``. Consider the
|
||||
following remote schema.
|
||||
|
||||
.. code-block:: graphql
|
||||
|
||||
type Query {
|
||||
get_coordinates(city: String): Coordinates
|
||||
}
|
||||
type Coordinates{
|
||||
lat: Float
|
||||
long: Float
|
||||
}
|
||||
|
||||
|
||||
:ref:`Define a remote relationship<create_remote_relationship>` with name ``user_location`` from ``user_city``
|
||||
scalar computed field to ``get_coordinates`` remote schema field. We can query users with the pincode of their residing place.
|
||||
|
||||
.. graphiql::
|
||||
:view_only:
|
||||
:query:
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
user_city
|
||||
user_location
|
||||
}
|
||||
}
|
||||
:response:
|
||||
{
|
||||
"data": {
|
||||
"authors": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"user_city": "Frisco",
|
||||
"user_location": {
|
||||
"lat": 33.155373,
|
||||
"long": -96.818733
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Only ``Scalar computed fields`` are allowed to join fields from remote sources
|
||||
|
||||
.. admonition:: Supported from
|
||||
|
||||
This feature is available in ``v2.0.1`` and above
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@ -901,7 +901,7 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
|
||||
processArrayRelation (mkSourcePrefixes arrRelSourcePrefix) fieldName arrRelAlias arrSel
|
||||
pure $ S.mkQIdenExp arrRelSourcePrefix fieldName
|
||||
|
||||
AFComputedField _ (CFSScalar scalar caseBoolExpMaybe) -> do
|
||||
AFComputedField _ _ (CFSScalar scalar caseBoolExpMaybe) -> do
|
||||
computedFieldSQLExp <- fromScalarComputedField scalar
|
||||
-- The computed field is conditionally outputed depending
|
||||
-- on the presence of `caseBoolExpMaybe` and the value it
|
||||
@ -915,7 +915,7 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
|
||||
$ _accColCaseBoolExpField <$> caseBoolExp
|
||||
in pure $ S.SECond boolExp computedFieldSQLExp S.SENull
|
||||
|
||||
AFComputedField _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
|
||||
AFComputedField _ _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
|
||||
let computedFieldSourcePrefix =
|
||||
mkComputedFieldTableAlias sourcePrefix fieldName
|
||||
(selectSource, nodeExtractors) <-
|
||||
|
@ -47,7 +47,7 @@ import qualified Hasura.Tracing as Tracing
|
||||
import Hasura.Base.Error
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.ParameterizedQueryHash
|
||||
import Hasura.GraphQL.Parser.Column (UnpreparedValue)
|
||||
import Hasura.GraphQL.Parser.Column (UnpreparedValue (..))
|
||||
import Hasura.GraphQL.Parser.Directives
|
||||
import Hasura.GraphQL.Parser.Monad
|
||||
import Hasura.GraphQL.RemoteServer (execRemoteGQ)
|
||||
|
@ -43,6 +43,7 @@ import Data.Has
|
||||
import Data.IORef
|
||||
import Data.Set (Set)
|
||||
import Data.Text.Extended
|
||||
import Data.Text.NonEmpty
|
||||
|
||||
import qualified Hasura.Backends.Postgres.SQL.DML as S
|
||||
import qualified Hasura.Backends.Postgres.Translate.Select as RS
|
||||
@ -243,7 +244,7 @@ resolveAsyncActionQuery userInfo annAction =
|
||||
AsyncTypename t -> RS.AFExpression t
|
||||
AsyncOutput annFields ->
|
||||
let inputTableArgument = RS.AETableRow $ Just $ Identifier "response_payload"
|
||||
in RS.AFComputedField ()
|
||||
in RS.AFComputedField () (ComputedFieldName $$(nonEmptyText "__action_computed_field"))
|
||||
$ RS.CFSTable jsonAggSelect
|
||||
$ processOutputSelectionSet inputTableArgument outputType definitionList annFields stringifyNumerics
|
||||
|
||||
|
@ -19,6 +19,7 @@ import qualified Data.List.NonEmpty as NE
|
||||
import Control.Lens
|
||||
|
||||
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
||||
import Hasura.GraphQL.Parser.Column (UnpreparedValue (..))
|
||||
import Hasura.RQL.IR
|
||||
import Hasura.RQL.IR.Returning
|
||||
import Hasura.RQL.Types
|
||||
@ -51,9 +52,9 @@ import Hasura.RQL.Types
|
||||
|
||||
-- | Collects remote joins from the AST and also adds the necessary join fields
|
||||
getRemoteJoins
|
||||
:: Backend b
|
||||
=> QueryDB b r u
|
||||
-> (QueryDB b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> QueryDB b r (UnpreparedValue b)
|
||||
-> (QueryDB b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoins = \case
|
||||
QDBMultipleRows s -> first QDBMultipleRows $ getRemoteJoinsSelect s
|
||||
QDBSingleRow s -> first QDBSingleRow $ getRemoteJoinsSelect s
|
||||
@ -62,33 +63,33 @@ getRemoteJoins = \case
|
||||
|
||||
-- | Traverse through 'AnnSimpleSel' and collect remote join fields (if any).
|
||||
getRemoteJoinsSelect
|
||||
:: Backend b
|
||||
=> AnnSimpleSelectG b r u
|
||||
-> (AnnSimpleSelectG b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> AnnSimpleSelectG b r (UnpreparedValue b)
|
||||
-> (AnnSimpleSelectG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsSelect =
|
||||
second mapToNonEmpty . flip runState mempty . transformSelect mempty
|
||||
|
||||
-- | Traverse through @'AnnAggregateSelect' and collect remote join fields (if any).
|
||||
getRemoteJoinsAggregateSelect
|
||||
:: Backend b
|
||||
=> AnnAggregateSelectG b r u
|
||||
-> (AnnAggregateSelectG b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> AnnAggregateSelectG b r (UnpreparedValue b)
|
||||
-> (AnnAggregateSelectG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsAggregateSelect =
|
||||
second mapToNonEmpty . flip runState mempty . transformAggregateSelect mempty
|
||||
|
||||
-- | Traverse through @'ConnectionSelect' and collect remote join fields (if any).
|
||||
getRemoteJoinsConnectionSelect
|
||||
:: Backend b
|
||||
=> ConnectionSelect b r u
|
||||
-> (ConnectionSelect b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> ConnectionSelect b r (UnpreparedValue b)
|
||||
-> (ConnectionSelect b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsConnectionSelect =
|
||||
second mapToNonEmpty . flip runState mempty . transformConnectionSelect mempty
|
||||
|
||||
-- | Traverse through 'MutationOutput' and collect remote join fields (if any)
|
||||
getRemoteJoinsMutationOutput
|
||||
:: Backend b
|
||||
=> MutationOutputG b r u
|
||||
-> (MutationOutputG b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> MutationOutputG b r (UnpreparedValue b)
|
||||
-> (MutationOutputG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsMutationOutput =
|
||||
second mapToNonEmpty . flip runState mempty . transformMutationOutput mempty
|
||||
where
|
||||
@ -110,16 +111,16 @@ getRemoteJoinsMutationOutput =
|
||||
-- local helpers
|
||||
|
||||
getRemoteJoinsAnnFields
|
||||
:: Backend b
|
||||
=> AnnFieldsG b r u
|
||||
-> (AnnFieldsG b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> AnnFieldsG b r (UnpreparedValue b)
|
||||
-> (AnnFieldsG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsAnnFields =
|
||||
second mapToNonEmpty . flip runState mempty . transformAnnFields mempty
|
||||
|
||||
getRemoteJoinsMutationDB
|
||||
:: Backend b
|
||||
=> MutationDB b r u
|
||||
-> (MutationDB b (Const Void) u, Maybe RemoteJoins)
|
||||
:: (Backend b)
|
||||
=> MutationDB b r (UnpreparedValue b)
|
||||
-> (MutationDB b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsMutationDB = \case
|
||||
MDBInsert insert ->
|
||||
first MDBInsert $ getRemoteJoinsInsert insert
|
||||
@ -144,16 +145,16 @@ getRemoteJoinsMutationDB = \case
|
||||
|
||||
getRemoteJoinsSyncAction
|
||||
:: (Backend b)
|
||||
=> AnnActionExecution b r v
|
||||
-> (AnnActionExecution b (Const Void) v, Maybe RemoteJoins)
|
||||
=> AnnActionExecution b r (UnpreparedValue b)
|
||||
-> (AnnActionExecution b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsSyncAction actionExecution =
|
||||
let (fields', remoteJoins) = getRemoteJoinsAnnFields $ _aaeFields actionExecution
|
||||
in (actionExecution { _aaeFields = fields' }, remoteJoins)
|
||||
|
||||
getRemoteJoinsActionQuery
|
||||
:: (Backend b)
|
||||
=> ActionQuery b r v
|
||||
-> (ActionQuery b (Const Void) v, Maybe RemoteJoins)
|
||||
=> ActionQuery b r (UnpreparedValue b)
|
||||
-> (ActionQuery b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsActionQuery = \case
|
||||
AQQuery sync ->
|
||||
first AQQuery $ getRemoteJoinsSyncAction sync
|
||||
@ -179,19 +180,18 @@ getRemoteJoinsActionQuery = \case
|
||||
|
||||
getRemoteJoinsActionMutation
|
||||
:: (Backend b)
|
||||
=> ActionMutation b r v
|
||||
-> (ActionMutation b (Const Void) v, Maybe RemoteJoins)
|
||||
=> ActionMutation b r (UnpreparedValue b)
|
||||
-> (ActionMutation b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
|
||||
getRemoteJoinsActionMutation = \case
|
||||
AMSync sync ->
|
||||
first AMSync $ getRemoteJoinsSyncAction sync
|
||||
AMSync sync -> first AMSync $ getRemoteJoinsSyncAction sync
|
||||
AMAsync async -> (AMAsync async, Nothing)
|
||||
|
||||
|
||||
transformSelect
|
||||
:: Backend b
|
||||
:: (Backend b)
|
||||
=> FieldPath
|
||||
-> AnnSimpleSelectG b r u
|
||||
-> State RemoteJoinMap (AnnSimpleSelectG b (Const Void) u)
|
||||
-> AnnSimpleSelectG b r (UnpreparedValue b)
|
||||
-> State RemoteJoinMap (AnnSimpleSelectG b (Const Void) (UnpreparedValue b))
|
||||
transformSelect path sel = do
|
||||
let fields = _asnFields sel
|
||||
-- Transform selects in array, object and computed fields
|
||||
@ -199,10 +199,10 @@ transformSelect path sel = do
|
||||
pure sel{_asnFields = transformedFields}
|
||||
|
||||
transformAggregateSelect
|
||||
:: Backend b
|
||||
:: (Backend b)
|
||||
=> FieldPath
|
||||
-> AnnAggregateSelectG b r u
|
||||
-> State RemoteJoinMap (AnnAggregateSelectG b (Const Void) u)
|
||||
-> AnnAggregateSelectG b r (UnpreparedValue b)
|
||||
-> State RemoteJoinMap (AnnAggregateSelectG b (Const Void) (UnpreparedValue b))
|
||||
transformAggregateSelect path sel = do
|
||||
let aggFields = _asnFields sel
|
||||
transformedFields <- forM aggFields $ \(fieldName, aggField) ->
|
||||
@ -213,10 +213,10 @@ transformAggregateSelect path sel = do
|
||||
pure sel{_asnFields = transformedFields}
|
||||
|
||||
transformConnectionSelect
|
||||
:: Backend b
|
||||
:: (Backend b)
|
||||
=> FieldPath
|
||||
-> ConnectionSelect b r u
|
||||
-> State RemoteJoinMap (ConnectionSelect b (Const Void) u)
|
||||
-> ConnectionSelect b r (UnpreparedValue b)
|
||||
-> State RemoteJoinMap (ConnectionSelect b (Const Void) (UnpreparedValue b))
|
||||
transformConnectionSelect path ConnectionSelect{..} = do
|
||||
let connectionFields = _asnFields _csSelect
|
||||
transformedFields <- forM connectionFields $ \(fieldName, field) ->
|
||||
@ -236,21 +236,21 @@ transformConnectionSelect path ConnectionSelect{..} = do
|
||||
EdgeNode <$> transformAnnFields (appendPath fieldName edgePath) annFields
|
||||
|
||||
transformObjectSelect
|
||||
:: Backend b
|
||||
:: (Backend b)
|
||||
=> FieldPath
|
||||
-> AnnObjectSelectG b r u
|
||||
-> State RemoteJoinMap (AnnObjectSelectG b (Const Void) u)
|
||||
-> AnnObjectSelectG b r (UnpreparedValue b)
|
||||
-> State RemoteJoinMap (AnnObjectSelectG b (Const Void) (UnpreparedValue b))
|
||||
transformObjectSelect path sel = do
|
||||
let fields = _aosFields sel
|
||||
transformedFields <- transformAnnFields path fields
|
||||
pure sel{_aosFields = transformedFields}
|
||||
|
||||
transformAnnFields
|
||||
:: forall b r u
|
||||
. Backend b
|
||||
:: forall b r
|
||||
. (Backend b)
|
||||
=> FieldPath
|
||||
-> AnnFieldsG b r u
|
||||
-> State RemoteJoinMap (AnnFieldsG b (Const Void) u)
|
||||
-> AnnFieldsG b r (UnpreparedValue b)
|
||||
-> State RemoteJoinMap (AnnFieldsG b (Const Void) (UnpreparedValue b))
|
||||
transformAnnFields path fields = do
|
||||
|
||||
-- TODO: Check for correctness. I think this entire function seems to be
|
||||
@ -258,15 +258,21 @@ transformAnnFields path fields = do
|
||||
-- server, which is incorrect as they can be aliased. Similarly, the phantom
|
||||
-- columns are being added without checking for overlap with aliases
|
||||
|
||||
let pgColumnFields = HS.fromList $ map (pgiColumn . _acfInfo . snd) $
|
||||
getFields _AFColumn fields
|
||||
let columnsInSelSet = HS.fromList $ map (pgiColumn . _acfInfo . snd) $ getFields _AFColumn fields
|
||||
scalarComputedFieldsInSelSet = HS.fromList $ map ((^. _2) . snd) $ getFields _AFComputedField fields
|
||||
remoteSelects = getFields (_AFRemote) fields
|
||||
remoteJoins = remoteSelects <&> \(fieldName, remoteSelect) ->
|
||||
let RemoteSelect argsMap selSet hasuraColumns remoteFields rsi = remoteSelect
|
||||
hasuraColumnFields = HS.map (fromCol @b . pgiColumn) hasuraColumns
|
||||
phantomColumns = HS.filter ((`notElem` pgColumnFields) . pgiColumn) hasuraColumns
|
||||
in (phantomColumns, RemoteJoin fieldName argsMap selSet hasuraColumnFields remoteFields rsi $
|
||||
map (fromCol @b . pgiColumn) $ toList phantomColumns)
|
||||
let RemoteSelect argsMap selSet hasuraFields remoteFields rsi = remoteSelect
|
||||
hasuraFieldNames = HS.map dbJoinFieldToName hasuraFields
|
||||
|
||||
-- See Note [Phantom fields in Remote Joins]
|
||||
fieldPresentInSelection = \case
|
||||
JoinColumn columnInfo -> HS.member (pgiColumn columnInfo) columnsInSelSet
|
||||
JoinComputedField computedFieldInfo -> HS.member (_scfName computedFieldInfo) scalarComputedFieldsInSelSet
|
||||
|
||||
phantomFields = HS.filter (not . fieldPresentInSelection) hasuraFields
|
||||
phantomFieldNames = toList $ HS.map dbJoinFieldToName phantomFields
|
||||
in (phantomFields, RemoteJoin fieldName argsMap selSet hasuraFieldNames remoteFields rsi phantomFieldNames)
|
||||
|
||||
transformedFields <- forM fields $ \(fieldName, field') -> do
|
||||
let fieldPath = appendPath fieldName path
|
||||
@ -281,8 +287,8 @@ transformAnnFields path fields = do
|
||||
AFArrayRelation . ASAggregate <$> transformAnnRelation (transformAggregateSelect fieldPath) aggRel
|
||||
AFArrayRelation (ASConnection annRel) ->
|
||||
AFArrayRelation . ASConnection <$> transformAnnRelation (transformConnectionSelect fieldPath) annRel
|
||||
AFComputedField x computedField ->
|
||||
AFComputedField x <$> case computedField of
|
||||
AFComputedField x n computedField ->
|
||||
AFComputedField x n <$> case computedField of
|
||||
CFSScalar cfss cbe -> pure $ CFSScalar cfss cbe
|
||||
CFSTable jas annSel -> CFSTable jas <$> transformSelect fieldPath annSel
|
||||
AFRemote rs -> pure $ AFRemote rs
|
||||
@ -293,9 +299,13 @@ transformAnnFields path fields = do
|
||||
case NE.nonEmpty remoteJoins of
|
||||
Nothing -> pure transformedFields
|
||||
Just nonEmptyRemoteJoins -> do
|
||||
let phantomColumns = map (\ci -> (fromCol @b $ pgiColumn ci, AFColumn $ AnnColumnField ci False Nothing Nothing)) $ toList $ HS.unions $ map fst $ remoteJoins
|
||||
let annotatedPhantomFields = (toList $ HS.unions $ map fst remoteJoins) <&> \phantomField ->
|
||||
(dbJoinFieldToName phantomField,) $ case phantomField of
|
||||
JoinColumn columnInfo -> AFColumn $ AnnColumnField columnInfo False Nothing Nothing
|
||||
JoinComputedField computedFieldInfo -> mkScalarComputedFieldSelect computedFieldInfo
|
||||
|
||||
modify (Map.insert path $ fmap snd nonEmptyRemoteJoins)
|
||||
pure $ transformedFields <> phantomColumns
|
||||
pure $ transformedFields <> annotatedPhantomFields
|
||||
|
||||
where
|
||||
getFields f = mapMaybe (sequence . second (^? f))
|
||||
@ -304,6 +314,23 @@ transformAnnFields path fields = do
|
||||
transformedSelect <- f select
|
||||
pure $ AnnRelationSelectG name maps transformedSelect
|
||||
|
||||
mkScalarComputedFieldSelect :: ScalarComputedField b -> (AnnFieldG b (Const Void) (UnpreparedValue b))
|
||||
mkScalarComputedFieldSelect ScalarComputedField{..} =
|
||||
let functionArgs = flip FunctionArgsExp mempty
|
||||
$ functionArgsWithTableRowAndSession _scfTableArgument _scfSessionArgument
|
||||
fieldSelect = flip CFSScalar Nothing
|
||||
$ ComputedFieldScalarSelect _scfFunction functionArgs _scfType Nothing
|
||||
in AFComputedField _scfXField _scfName fieldSelect
|
||||
where
|
||||
functionArgsWithTableRowAndSession
|
||||
:: FunctionTableArgument
|
||||
-> Maybe FunctionSessionArgument
|
||||
-> [ArgumentExp b (UnpreparedValue b)]
|
||||
functionArgsWithTableRowAndSession _ Nothing = [AETableRow Nothing] -- No session argument
|
||||
functionArgsWithTableRowAndSession (FTAFirst) _ = [AETableRow Nothing, AESession UVSession]
|
||||
functionArgsWithTableRowAndSession (FTANamed _ 0) _ = [AETableRow Nothing, AESession UVSession] -- Index is 0 implies table argument is first
|
||||
functionArgsWithTableRowAndSession _ _ = [AESession UVSession, AETableRow Nothing]
|
||||
|
||||
|
||||
mapToNonEmpty :: RemoteJoinMap -> Maybe RemoteJoins
|
||||
mapToNonEmpty = NE.nonEmpty . Map.toList
|
||||
|
@ -22,6 +22,40 @@ newtype FieldPath = FieldPath {unFieldPath :: [FieldName]}
|
||||
appendPath :: FieldName -> FieldPath -> FieldPath
|
||||
appendPath fieldName = FieldPath . (<> [fieldName]) . unFieldPath
|
||||
|
||||
{- Note [Phantom fields in Remote Joins]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Usually, a join appears when we establish a relationship between two entities.
|
||||
In this case, a remote join is a relationship between a table from a database of
|
||||
any source to a remote GraphQL schema. The relationship is defined as native
|
||||
fields (column or scalar computed field) acts as input argument for a field in the
|
||||
remote schema. In order to make a call to remote schema we need to have values of
|
||||
the table fields. The fields may or may not be present in the actual selection set.
|
||||
If they aren't present, we need to fetch them explicitly from the database by inserting
|
||||
them in the generated SQL statement. Thus they're called phantom fields. In post-fetching
|
||||
joining process we need to remove the phantom fields and send the response to client.
|
||||
|
||||
Limitation for scalar computed fields:
|
||||
--------------------------------------
|
||||
We need to ensure a scalar computed field is to be included as phantom field if not
|
||||
present in the query selection set. But if the SQL function associated with the scalar
|
||||
computed field has input arguments other than table row and hasura session inputs, we
|
||||
cannot determine their values. Hence, we only accept scalar computed fields with no
|
||||
input arguments except table row and hasura session arguments in forming remote relationships.
|
||||
|
||||
Example: Let's say we have a computed field 'calculate_something' whose SQL function accepts
|
||||
input argument 'xfactor: Integer' other than table row input and hasura session argument.
|
||||
A remote relationship 'something_from_calculated' is defined and included in query selection set.
|
||||
The 'calculate_something' is absent in the selection set, so we need to include it in the phantom
|
||||
fields. Now, what's the value we should consider for the 'xfactor' input argument? So, we do
|
||||
restrict these scalar computed fields in forming the remote relationships at metadata API level.
|
||||
|
||||
Solution:
|
||||
--------
|
||||
A potential solution for aforementioned limitation is to update the create_remote_relationship
|
||||
metadata API to accept the computed fields with values of input arguments other than table row
|
||||
and hasura session arguments.
|
||||
-}
|
||||
|
||||
-- | A 'RemoteJoin' represents the context of remote relationship to be extracted from 'AnnFieldG's.
|
||||
data RemoteJoin
|
||||
= RemoteJoin
|
||||
@ -33,9 +67,8 @@ data RemoteJoin
|
||||
, _rjRemoteSchema :: !RemoteSchemaInfo -- ^ The remote schema server info.
|
||||
, _rjPhantomFields :: ![FieldName]
|
||||
-- ^ Hasura fields which are not in the selection set, but are required as
|
||||
-- parameters to satisfy the remote join.
|
||||
-- parameters to satisfy the remote join. See Note [Phantom fields in Remote Joins].
|
||||
} deriving (Eq)
|
||||
|
||||
type RemoteJoins = NE.NonEmpty (FieldPath, NE.NonEmpty RemoteJoin)
|
||||
type RemoteJoinMap = Map.HashMap FieldPath (NE.NonEmpty RemoteJoin)
|
||||
|
||||
|
@ -1043,7 +1043,7 @@ computedFieldPG sourceName ComputedFieldInfo{..} parentTable selectPermissions =
|
||||
fieldArgsParser = do
|
||||
args <- functionArgsParser
|
||||
colOp <- jsonPathArg $ ColumnScalar scalarReturnType
|
||||
pure $ IR.AFComputedField _cfiXComputedFieldInfo
|
||||
pure $ IR.AFComputedField _cfiXComputedFieldInfo _cfiName
|
||||
(IR.CFSScalar (IR.ComputedFieldScalarSelect
|
||||
{ IR._cfssFunction = _cffName _cfiFunction
|
||||
, IR._cfssType = scalarReturnType
|
||||
@ -1061,7 +1061,7 @@ computedFieldPG sourceName ComputedFieldInfo{..} parentTable selectPermissions =
|
||||
let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser
|
||||
pure $ P.subselection fieldName (Just fieldDescription) fieldArgsParser selectionSetParser <&>
|
||||
\((functionArgs', args), fields) ->
|
||||
IR.AFComputedField _cfiXComputedFieldInfo $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG
|
||||
IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG
|
||||
{ IR._asnFields = fields
|
||||
, IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing
|
||||
, IR._asnPerm = tablePermissionsInfo remotePerms
|
||||
@ -1112,7 +1112,7 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
|
||||
hoistMaybe $ Map.lookup remoteSchemaName remoteRelationshipQueryCtx
|
||||
let fieldDefns = map P.fDefinition (piQuery parsedIntrospection)
|
||||
role <- askRoleName
|
||||
let hasuraFieldNames = Set.map (FieldName . toTxt . pgiColumn) hasuraFields
|
||||
let hasuraFieldNames = Set.map dbJoinFieldToName hasuraFields
|
||||
remoteRelationship = RemoteRelationship name source table hasuraFieldNames remoteSchemaName remoteFields
|
||||
(newInpValDefns, remoteFieldParamMap) <-
|
||||
if | role == adminRoleName ->
|
||||
@ -1121,10 +1121,10 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
|
||||
-- was created
|
||||
pure (remoteSchemaInputValueDefns, _rfiParamMap remoteFieldInfo)
|
||||
| otherwise -> do
|
||||
fieldInfoMap <- (_tciFieldInfoMap . _tiCoreInfo) <$> askTableInfo @b source table
|
||||
roleRemoteField <-
|
||||
afold @(Either _) $
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, roleIntrospectionResult) $
|
||||
Set.toList hasuraFields
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, roleIntrospectionResult) fieldInfoMap
|
||||
pure $ (_rfiInputValueDefinitions roleRemoteField, _rfiParamMap roleRemoteField)
|
||||
let RemoteSchemaIntrospection typeDefns = irDoc roleIntrospectionResult
|
||||
-- add the new input value definitions created by the remote relationship
|
||||
@ -1153,7 +1153,7 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
|
||||
pure $ IR.AFRemote $ IR.RemoteSelect
|
||||
{ _rselArgs = remoteArgs
|
||||
, _rselSelection = selSet
|
||||
, _rselHasuraColumns = _rfiHasuraFields remoteFieldInfo
|
||||
, _rselHasuraFields = _rfiHasuraFields remoteFieldInfo
|
||||
, _rselFieldCall = unRemoteFields $ _rfiRemoteFields remoteFieldInfo
|
||||
, _rselRemoteSchema = _rfiRemoteSchema remoteFieldInfo
|
||||
}
|
||||
|
@ -87,18 +87,18 @@ buildRemoteFieldInfo
|
||||
:: forall m b
|
||||
. (Backend b, QErrM m)
|
||||
=> RemoteRelationship b
|
||||
-> [ColumnInfo b]
|
||||
-> FieldInfoMap (FieldInfo b)
|
||||
-> RemoteSchemaMap
|
||||
-> m (RemoteFieldInfo b, [SchemaDependency])
|
||||
buildRemoteFieldInfo remoteRelationship
|
||||
pgColumns
|
||||
fields
|
||||
remoteSchemaMap = do
|
||||
let remoteSchemaName = rtrRemoteSchema remoteRelationship
|
||||
(RemoteSchemaCtx _name introspectionResult remoteSchemaInfo _ _ _permissions) <-
|
||||
onNothing (Map.lookup remoteSchemaName remoteSchemaMap)
|
||||
$ throw400 RemoteSchemaError $ "remote schema with name " <> remoteSchemaName <<> " not found"
|
||||
eitherRemoteField <- runExceptT $
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) pgColumns
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) fields
|
||||
remoteField <- onLeft eitherRemoteField $ throw400 RemoteSchemaError . errorToText
|
||||
let table = rtrTable remoteRelationship
|
||||
source = rtrSource remoteRelationship
|
||||
@ -108,17 +108,13 @@ buildRemoteFieldInfo remoteRelationship
|
||||
$ AB.mkAnyBackend
|
||||
$ SOITable @b table)
|
||||
DRTable
|
||||
columnsDep =
|
||||
map
|
||||
(flip SchemaDependency DRRemoteRelationship
|
||||
. SOSourceObj source
|
||||
. AB.mkAnyBackend
|
||||
. SOITableObj @b table
|
||||
. TOCol @b
|
||||
. pgiColumn)
|
||||
$ S.toList $ _rfiHasuraFields remoteField
|
||||
fieldsDep = S.toList (_rfiHasuraFields remoteField) <&> \case
|
||||
JoinColumn columnInfo ->
|
||||
mkColDep @b DRRemoteRelationship source table $ pgiColumn columnInfo
|
||||
JoinComputedField computedFieldInfo ->
|
||||
mkComputedFieldDep @b DRRemoteRelationship source table $ _scfName computedFieldInfo
|
||||
remoteSchemaDep =
|
||||
SchemaDependency (SORemoteSchema remoteSchemaName) DRRemoteSchema
|
||||
in (tableDep : remoteSchemaDep : columnsDep)
|
||||
in (tableDep : remoteSchemaDep : fieldsDep)
|
||||
|
||||
pure (remoteField, schemaDependencies)
|
||||
|
@ -18,6 +18,7 @@ import Data.Text.Extended
|
||||
import Hasura.RQL.Types.Backend
|
||||
import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.ComputedField
|
||||
import Hasura.RQL.Types.RemoteRelationship
|
||||
import Hasura.RQL.Types.RemoteSchema
|
||||
import Hasura.RQL.Types.SchemaCache
|
||||
@ -34,13 +35,16 @@ data ValidationError (b :: BackendType)
|
||||
| TypeNotFound !G.Name
|
||||
| TableNotFound !(TableName b)
|
||||
| TableFieldNonexistent !(TableName b) !FieldName
|
||||
| TableFieldNotSupported !FieldName
|
||||
| TableComputedFieldWithInputArgs !FieldName !(FunctionName b)
|
||||
| ExpectedTypeButGot !G.GType !G.GType
|
||||
| InvalidType !G.GType !Text
|
||||
| InvalidVariable !G.Name !(HM.HashMap G.Name (ColumnInfo b))
|
||||
| InvalidVariable !G.Name !(HM.HashMap G.Name (DBJoinField b))
|
||||
| NullNotAllowedHere
|
||||
| InvalidGTypeForStripping !G.GType
|
||||
| UnsupportedMultipleElementLists
|
||||
| UnsupportedEnum
|
||||
| UnsupportedTableComputedField !(TableName b) !ComputedFieldName
|
||||
| InvalidGraphQLName !Text
|
||||
| IDTypeJoin !G.Name
|
||||
-- | This is the case where the type of the columns that are mapped do not
|
||||
@ -70,6 +74,11 @@ errorToText = \case
|
||||
"table with name " <> name <<> " not found"
|
||||
TableFieldNonexistent table fieldName ->
|
||||
"field with name " <> fieldName <<> " not found in table " <>> table
|
||||
TableFieldNotSupported fieldName ->
|
||||
"field with name " <> fieldName <<> " not supported; only columns and scalar computed fields"
|
||||
TableComputedFieldWithInputArgs fieldName function ->
|
||||
"computed field " <> fieldName <<> " is associated with SQL function " <> function
|
||||
<<> " has input arguments other than table row type and hasura session"
|
||||
ExpectedTypeButGot expTy actualTy ->
|
||||
"expected type " <> G.getBaseType expTy <<> " but got " <>> G.getBaseType actualTy
|
||||
InvalidType ty err ->
|
||||
@ -84,6 +93,8 @@ errorToText = \case
|
||||
"multiple elements in list value is not supported"
|
||||
UnsupportedEnum ->
|
||||
"enum value is not supported"
|
||||
UnsupportedTableComputedField tableName fieldName ->
|
||||
"computed field " <> fieldName <<> " returns set of " <> tableName <<> ", is not supported"
|
||||
InvalidGraphQLName t ->
|
||||
t <<> " is not a valid GraphQL identifier"
|
||||
IDTypeJoin typeName ->
|
||||
@ -99,26 +110,34 @@ validateRemoteRelationship
|
||||
. (Backend b, MonadError (ValidationError b) m)
|
||||
=> RemoteRelationship b
|
||||
-> (RemoteSchemaInfo, IntrospectionResult)
|
||||
-> [ColumnInfo b]
|
||||
-> FieldInfoMap (FieldInfo b)
|
||||
-> m (RemoteFieldInfo b)
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) pgColumns = do
|
||||
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) fields = do
|
||||
let remoteSchemaName = rtrRemoteSchema remoteRelationship
|
||||
table = rtrTable remoteRelationship
|
||||
hasuraFields <- forM (toList $ rtrHasuraFields remoteRelationship) $
|
||||
\fieldName -> onNothing (find ((==) fieldName . fromCol @b . pgiColumn) pgColumns) $
|
||||
throwError $ TableFieldNonexistent table fieldName
|
||||
pgColumnsVariables <- mapM (\(k,v) -> do
|
||||
variableName <- pgColumnToVariable k
|
||||
pure $ (variableName,v)
|
||||
) $ HM.toList (mapFromL pgiColumn pgColumns)
|
||||
let pgColumnsVariablesMap = HM.fromList pgColumnsVariables
|
||||
hasuraFields <- forM (toList $ rtrHasuraFields remoteRelationship) $ \fieldName -> do
|
||||
fieldInfo <- onNothing (HM.lookup fieldName fields) $ throwError $ TableFieldNonexistent table fieldName
|
||||
case fieldInfo of
|
||||
FIColumn columnInfo -> pure $ JoinColumn columnInfo
|
||||
FIComputedField ComputedFieldInfo{..} -> do
|
||||
scalarType <- case _cfiReturnType of
|
||||
CFRScalar ty -> pure ty
|
||||
CFRSetofTable{} -> throwError $ UnsupportedTableComputedField table _cfiName
|
||||
let ComputedFieldFunction{..} = _cfiFunction
|
||||
case toList _cffInputArgs of
|
||||
[] -> pure $ JoinComputedField $ ScalarComputedField _cfiXComputedFieldInfo _cfiName _cffName
|
||||
_cffTableArgument _cffSessionArgument scalarType
|
||||
_ -> throwError $ TableComputedFieldWithInputArgs fieldName _cffName
|
||||
_ -> throwError $ TableFieldNotSupported fieldName
|
||||
hasuraFieldsVariablesMap <-
|
||||
fmap HM.fromList $ for hasuraFields $ \field -> (, field) <$> hasuraFieldToVariable field
|
||||
let schemaDoc = irDoc introspectionResult
|
||||
queryRootName = irQueryRoot introspectionResult
|
||||
queryRoot <- onNothing (lookupObject schemaDoc queryRootName) $
|
||||
throwError $ FieldNotFoundInRemoteSchema queryRootName
|
||||
(_, (leafParamMap, leafTypeMap)) <-
|
||||
foldlM
|
||||
(buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc)
|
||||
(buildRelationshipTypeInfo hasuraFieldsVariablesMap schemaDoc)
|
||||
(queryRoot, (mempty, mempty))
|
||||
(unRemoteFields $ rtrRemoteField remoteRelationship)
|
||||
pure $ RemoteFieldInfo
|
||||
@ -153,7 +172,7 @@ validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionRe
|
||||
_ -> False
|
||||
|
||||
buildRelationshipTypeInfo
|
||||
:: HashMap G.Name (ColumnInfo b)
|
||||
:: HashMap G.Name (DBJoinField b)
|
||||
-> RemoteSchemaIntrospection
|
||||
-> (G.ObjectTypeDefinition RemoteSchemaInputValueDefinition,
|
||||
( (HashMap G.Name RemoteSchemaInputValueDefinition)
|
||||
@ -162,13 +181,13 @@ validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionRe
|
||||
-> m ( G.ObjectTypeDefinition RemoteSchemaInputValueDefinition
|
||||
, ( HashMap G.Name RemoteSchemaInputValueDefinition
|
||||
, HashMap G.Name (G.TypeDefinition [G.Name] RemoteSchemaInputValueDefinition)))
|
||||
buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc (objTyInfo,(_,typeMap)) fieldCall = do
|
||||
buildRelationshipTypeInfo hasuraFieldsVariablesMap schemaDoc (objTyInfo,(_,typeMap)) fieldCall = do
|
||||
objFldDefinition <- lookupField (fcName fieldCall) objTyInfo
|
||||
let providedArguments = getRemoteArguments $ fcArguments fieldCall
|
||||
(validateRemoteArguments
|
||||
(mapFromL (G._ivdName . _rsitdDefinition) (G._fldArgumentsDefinition objFldDefinition))
|
||||
providedArguments
|
||||
pgColumnsVariablesMap
|
||||
hasuraFieldsVariablesMap
|
||||
schemaDoc)
|
||||
let eitherParamAndTypeMap =
|
||||
runStateT
|
||||
@ -326,13 +345,15 @@ renameNamedType rename =
|
||||
G.unsafeMkName . rename . G.unName
|
||||
|
||||
-- | Convert a field name to a variable name.
|
||||
pgColumnToVariable
|
||||
hasuraFieldToVariable
|
||||
:: (Backend b, MonadError (ValidationError b) m)
|
||||
=> (Column b)
|
||||
=> (DBJoinField b)
|
||||
-> m G.Name
|
||||
pgColumnToVariable pgCol =
|
||||
let pgColText = toTxt pgCol
|
||||
in G.mkName pgColText `onNothing` throwError (InvalidGraphQLName pgColText)
|
||||
hasuraFieldToVariable hasuraField = do
|
||||
let fieldText = case hasuraField of
|
||||
JoinColumn columnInfo -> toTxt $ pgiColumn columnInfo
|
||||
JoinComputedField computedFieldInfo -> toTxt $ _scfName computedFieldInfo
|
||||
G.mkName fieldText `onNothing` throwError (InvalidGraphQLName fieldText)
|
||||
|
||||
-- | Lookup the field in the schema.
|
||||
lookupField
|
||||
@ -354,7 +375,7 @@ validateRemoteArguments
|
||||
:: (Backend b, MonadError (ValidationError b) m)
|
||||
=> HM.HashMap G.Name RemoteSchemaInputValueDefinition
|
||||
-> HM.HashMap G.Name (G.Value G.Name)
|
||||
-> HM.HashMap G.Name (ColumnInfo b)
|
||||
-> HM.HashMap G.Name (DBJoinField b)
|
||||
-> RemoteSchemaIntrospection
|
||||
-> m ()
|
||||
validateRemoteArguments expectedArguments providedArguments permittedVariables schemaDocument = do
|
||||
@ -376,7 +397,7 @@ unwrapGraphQLType = \case
|
||||
-- | Validate a value against a type.
|
||||
validateType
|
||||
:: (Backend b, MonadError (ValidationError b) m)
|
||||
=> HM.HashMap G.Name (ColumnInfo b)
|
||||
=> HM.HashMap G.Name (DBJoinField b)
|
||||
-> G.Value G.Name
|
||||
-> G.GType
|
||||
-> RemoteSchemaIntrospection
|
||||
@ -387,7 +408,7 @@ validateType permittedVariables value expectedGType schemaDocument =
|
||||
case HM.lookup variable permittedVariables of
|
||||
Nothing -> throwError (InvalidVariable variable permittedVariables)
|
||||
Just fieldInfo -> do
|
||||
namedType <- columnInfoToNamedType fieldInfo
|
||||
namedType <- dbJoinFieldToNamedType fieldInfo
|
||||
isTypeCoercible (mkGraphQLType namedType) expectedGType
|
||||
G.VInt {} -> do
|
||||
let intScalarGType = mkGraphQLType intScalar
|
||||
@ -477,17 +498,21 @@ assertListType actualType =
|
||||
(throwError $ InvalidType actualType "is not a list type")
|
||||
|
||||
-- | Convert a field info to a named type, if possible.
|
||||
columnInfoToNamedType
|
||||
dbJoinFieldToNamedType
|
||||
:: forall b m .
|
||||
(Backend b, MonadError (ValidationError b) m)
|
||||
=> ColumnInfo b
|
||||
=> DBJoinField b
|
||||
-> m G.Name
|
||||
columnInfoToNamedType pci =
|
||||
case pgiType pci of
|
||||
ColumnScalar scalarType ->
|
||||
onLeft (scalarTypeGraphQLName @b scalarType)
|
||||
(const $ throwError $ CannotGenerateGraphQLTypeName scalarType)
|
||||
_ -> throwError UnsupportedEnum
|
||||
dbJoinFieldToNamedType hasuraField = do
|
||||
scalarType <- case hasuraField of
|
||||
JoinColumn pci -> case pgiType pci of
|
||||
ColumnScalar scalarType -> pure scalarType
|
||||
_ -> throwError UnsupportedEnum
|
||||
JoinComputedField cfi -> pure $ _scfType cfi
|
||||
-- CFRScalar scalarType -> pure scalarType
|
||||
-- CFRSetofTable table -> throwError $ UnsupportedTableComputedField table $ _cfiName cfi
|
||||
onLeft (scalarTypeGraphQLName @b scalarType) $
|
||||
const $ throwError $ CannotGenerateGraphQLTypeName scalarType
|
||||
|
||||
getBaseTyWithNestedLevelsCount :: G.GType -> (G.Name, Int)
|
||||
getBaseTyWithNestedLevelsCount ty = go ty 0
|
||||
|
@ -69,12 +69,18 @@ addNonColumnFields = proc ( source
|
||||
buildComputedField
|
||||
-< (HS.fromList $ M.keys rawTableInfo, map (source, pgFunctions, _nctiTable,) _nctiComputedFields)
|
||||
|
||||
let columnsAndComputedFields =
|
||||
let columnFields = columns <&> FIColumn
|
||||
computedFields = M.fromList $ flip map (M.toList computedFieldInfos) $
|
||||
\(cfName, (cfInfo, _)) -> (fromComputedField cfName, FIComputedField cfInfo)
|
||||
in M.union columnFields computedFields
|
||||
|
||||
rawRemoteRelationshipInfos
|
||||
<- buildInfoMapPreservingMetadata
|
||||
(_rrmName . (^. _3))
|
||||
(mkRemoteRelationshipMetadataObject @b)
|
||||
buildRemoteRelationship
|
||||
-< ((M.elems columns, remoteSchemaMap), map (source, _nctiTable,) _nctiRemoteRelationships)
|
||||
-< ((columnsAndComputedFields, remoteSchemaMap), map (source, _nctiTable,) _nctiRemoteRelationships)
|
||||
|
||||
let relationshipFields = mapKeys fromRel relationshipInfos
|
||||
computedFieldFields = mapKeys fromComputedField computedFieldInfos
|
||||
@ -251,7 +257,7 @@ buildRemoteRelationship
|
||||
:: forall b arr m
|
||||
. ( ArrowChoice arr, ArrowWriter (Seq CollectedInfo) arr
|
||||
, ArrowKleisli m arr, MonadError QErr m, BackendMetadata b)
|
||||
=> ( ([ColumnInfo b], RemoteSchemaMap)
|
||||
=> ( (FieldInfoMap (FieldInfo b), RemoteSchemaMap)
|
||||
, (SourceName, TableName b, RemoteRelationshipMetadata)
|
||||
) `arr` Maybe (RemoteFieldInfo b)
|
||||
buildRemoteRelationship = proc ( (pgColumns, remoteSchemaMap)
|
||||
|
@ -45,6 +45,7 @@ import Hasura.RQL.IR.OrderBy
|
||||
import Hasura.RQL.Types.Backend
|
||||
import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.ComputedField
|
||||
import Hasura.RQL.Types.Function
|
||||
import Hasura.RQL.Types.Instances ()
|
||||
import Hasura.RQL.Types.Relationship
|
||||
@ -186,7 +187,7 @@ data AnnFieldG (b :: BackendType) (r :: BackendType -> Type) v
|
||||
= AFColumn !(AnnColumnField b v)
|
||||
| AFObjectRelation !(ObjectRelationSelectG b r v)
|
||||
| AFArrayRelation !(ArraySelectG b r v)
|
||||
| AFComputedField !(XComputedField b) !(ComputedFieldSelect b r v)
|
||||
| AFComputedField !(XComputedField b) !ComputedFieldName !(ComputedFieldSelect b r v)
|
||||
| AFRemote !(RemoteSelect b)
|
||||
| AFDBRemote !(AB.AnyBackend (DBRemoteSelect b r))
|
||||
| AFNodeId !(XRelay b) !(TableName b) !(PrimaryKeyColumns b)
|
||||
@ -210,7 +211,6 @@ mkAnnColumnFieldAsText
|
||||
mkAnnColumnFieldAsText ci =
|
||||
AFColumn (AnnColumnField ci True Nothing Nothing)
|
||||
|
||||
|
||||
-- Aggregation fields
|
||||
|
||||
data TableAggregateFieldG (b :: BackendType) (r :: BackendType -> Type) v
|
||||
@ -270,7 +270,6 @@ type ConnectionFields b r v = Fields (ConnectionField b r v)
|
||||
type PageInfoFields = Fields PageInfoField
|
||||
type EdgeFields b r v = Fields (EdgeField b r v)
|
||||
|
||||
|
||||
-- Column
|
||||
|
||||
data AnnColumnField (b :: BackendType) v
|
||||
@ -372,11 +371,11 @@ data RemoteFieldArgument
|
||||
|
||||
data RemoteSelect (b :: BackendType)
|
||||
= RemoteSelect
|
||||
{ _rselArgs :: ![RemoteFieldArgument]
|
||||
, _rselSelection :: !(G.SelectionSet G.NoFragments RemoteSchemaVariable)
|
||||
, _rselHasuraColumns :: !(HashSet (ColumnInfo b))
|
||||
, _rselFieldCall :: !(NonEmpty FieldCall)
|
||||
, _rselRemoteSchema :: !RemoteSchemaInfo
|
||||
{ _rselArgs :: ![RemoteFieldArgument]
|
||||
, _rselSelection :: !(G.SelectionSet G.NoFragments RemoteSchemaVariable)
|
||||
, _rselHasuraFields :: !(HashSet (DBJoinField b))
|
||||
, _rselFieldCall :: !(NonEmpty FieldCall)
|
||||
, _rselRemoteSchema :: !RemoteSchemaInfo
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,6 +3,9 @@ module Hasura.RQL.Types.RemoteRelationship
|
||||
, remoteRelationshipNameToText
|
||||
, fromRemoteRelationship
|
||||
, RemoteFields(..)
|
||||
, ScalarComputedField(..)
|
||||
, DBJoinField(..)
|
||||
, dbJoinFieldToName
|
||||
, RemoteFieldInfo(..)
|
||||
, RemoteRelationship(..)
|
||||
, RemoteRelationshipDef(..)
|
||||
@ -17,22 +20,23 @@ module Hasura.RQL.Types.RemoteRelationship
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Control.Lens (makeLenses)
|
||||
import Control.Lens (makeLenses)
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
import Data.Scientific
|
||||
import Data.Text.Extended
|
||||
import Data.Text.NonEmpty
|
||||
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.RQL.Types.Backend
|
||||
import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.ComputedField
|
||||
import Hasura.RQL.Types.RemoteSchema
|
||||
import Hasura.SQL.Backend
|
||||
|
||||
@ -50,6 +54,48 @@ remoteRelationshipNameToText = unNonEmptyText . unRemoteRelationshipName
|
||||
fromRemoteRelationship :: RemoteRelationshipName -> FieldName
|
||||
fromRemoteRelationship = FieldName . remoteRelationshipNameToText
|
||||
|
||||
data ScalarComputedField (b :: BackendType)
|
||||
= ScalarComputedField
|
||||
{ _scfXField :: !(XComputedField b)
|
||||
, _scfName :: !ComputedFieldName
|
||||
, _scfFunction :: !(FunctionName b)
|
||||
, _scfTableArgument :: !FunctionTableArgument
|
||||
, _scfSessionArgument :: !(Maybe FunctionSessionArgument)
|
||||
, _scfType :: !(ScalarType b)
|
||||
} deriving (Generic)
|
||||
deriving instance Backend b => Eq (ScalarComputedField b)
|
||||
deriving instance Backend b => Show (ScalarComputedField b)
|
||||
instance Backend b => Cacheable (ScalarComputedField b)
|
||||
instance Backend b => Hashable (ScalarComputedField b)
|
||||
|
||||
instance Backend b => ToJSON (ScalarComputedField b) where
|
||||
toJSON ScalarComputedField{..} =
|
||||
object [ "name" .= _scfName
|
||||
, "function" .= _scfFunction
|
||||
, "table_argument" .= _scfTableArgument
|
||||
, "session_argument" .= _scfSessionArgument
|
||||
, "type" .= _scfType
|
||||
]
|
||||
|
||||
data DBJoinField (b :: BackendType)
|
||||
= JoinColumn !(ColumnInfo b)
|
||||
| JoinComputedField !(ScalarComputedField b)
|
||||
deriving (Generic)
|
||||
deriving instance Backend b => Eq (DBJoinField b)
|
||||
deriving instance Backend b => Show (DBJoinField b)
|
||||
instance Backend b => Cacheable (DBJoinField b)
|
||||
instance Backend b => Hashable (DBJoinField b)
|
||||
|
||||
instance (Backend b) => ToJSON (DBJoinField b) where
|
||||
toJSON = \case
|
||||
JoinColumn columnInfo -> toJSON columnInfo
|
||||
JoinComputedField computedField -> toJSON computedField
|
||||
|
||||
dbJoinFieldToName :: forall b. (Backend b) => DBJoinField b -> FieldName
|
||||
dbJoinFieldToName = \case
|
||||
JoinColumn columnInfo -> fromCol @b $ pgiColumn $ columnInfo
|
||||
JoinComputedField computedFieldInfo -> fromComputedField $ _scfName computedFieldInfo
|
||||
|
||||
-- | Resolved remote relationship
|
||||
data RemoteFieldInfo (b :: BackendType)
|
||||
= RemoteFieldInfo
|
||||
@ -61,7 +107,7 @@ data RemoteFieldInfo (b :: BackendType)
|
||||
-- include the arguments to the remote field that is being joined. The
|
||||
-- names of the arguments here are modified, it will be in the format of
|
||||
-- <Original Field Name>_remote_rel_<hasura table schema>_<hasura table name><remote relationship name>
|
||||
, _rfiHasuraFields :: !(HashSet (ColumnInfo b))
|
||||
, _rfiHasuraFields :: !(HashSet (DBJoinField b))
|
||||
-- ^ Hasura fields used to join the remote schema node
|
||||
, _rfiRemoteFields :: !RemoteFields
|
||||
, _rfiRemoteSchema :: !RemoteSchemaInfo
|
||||
@ -242,9 +288,9 @@ data RemoteRelationship b =
|
||||
, rtrTable :: !(TableName b)
|
||||
-- ^ (SourceName, QualifiedTable) determines the table on which the relationship
|
||||
-- is defined
|
||||
, rtrHasuraFields :: !(HashSet FieldName) -- TODO change to PGCol
|
||||
, rtrHasuraFields :: !(HashSet FieldName)
|
||||
-- ^ The hasura fields from 'rtrTable' that will be in scope when resolving
|
||||
-- the remote objects in 'rtrRemoteField'.
|
||||
-- the remote objects in 'rtrRemoteField'. Supports columns and computed fields.
|
||||
, rtrRemoteSchema :: !RemoteSchemaName
|
||||
-- ^ Identifier for this mapping.
|
||||
, rtrRemoteField :: !RemoteFields
|
||||
|
@ -0,0 +1,21 @@
|
||||
description: Fetch remote join involving a computed field
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
students{
|
||||
id
|
||||
name
|
||||
grade
|
||||
}
|
||||
}
|
||||
response:
|
||||
data:
|
||||
students:
|
||||
- id: 1
|
||||
name: alice
|
||||
grade: S
|
||||
- id: 2
|
||||
name: bob
|
||||
grade: B
|
@ -0,0 +1,24 @@
|
||||
description: Fetch remote join involving a computed field with session argument
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
x-hasura-offset: '10'
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
students{
|
||||
id
|
||||
name
|
||||
grade_session
|
||||
}
|
||||
}
|
||||
response:
|
||||
data:
|
||||
students:
|
||||
- id: 1
|
||||
name: alice
|
||||
grade_session: A
|
||||
- id: 2
|
||||
name: bob
|
||||
grade_session: B
|
@ -35,6 +35,25 @@ args:
|
||||
name text
|
||||
);
|
||||
insert into employees (name) values ('alice'),(NULL),('bob');
|
||||
create table students (
|
||||
id serial primary key,
|
||||
name text not null,
|
||||
physics integer,
|
||||
maths integer
|
||||
);
|
||||
insert into students (name, physics, maths) values ('alice', 45, 48), ('bob', 32, 40);
|
||||
create function total_marks(student_row students)
|
||||
returns integer as $$
|
||||
select student_row.physics + student_row.maths
|
||||
$$ language sql stable;
|
||||
create function total_marks_offset(student_row students, "offset" integer)
|
||||
returns integer as $$
|
||||
select student_row.physics + student_row.maths - "offset"
|
||||
$$ language sql stable;
|
||||
create function total_marks_session(student_row students, hasura_session json)
|
||||
returns integer as $$
|
||||
select student_row.physics + student_row.maths - (hasura_session ->> 'x-hasura-offset')::integer
|
||||
$$ language sql stable;
|
||||
|
||||
- type: track_table
|
||||
args:
|
||||
@ -57,3 +76,33 @@ args:
|
||||
args:
|
||||
schema: public
|
||||
name: employees
|
||||
|
||||
- type: track_table
|
||||
args:
|
||||
schema: public
|
||||
name: students
|
||||
|
||||
- type: add_computed_field
|
||||
args:
|
||||
table: students
|
||||
name: total_marks
|
||||
definition:
|
||||
function: total_marks
|
||||
table_argument: student_row
|
||||
|
||||
- type: add_computed_field
|
||||
args:
|
||||
table: students
|
||||
name: total_marks_offset
|
||||
definition:
|
||||
function: total_marks_offset
|
||||
table_argument: student_row
|
||||
|
||||
- type: add_computed_field
|
||||
args:
|
||||
table: students
|
||||
name: total_marks_session
|
||||
definition:
|
||||
function: total_marks_session
|
||||
table_argument: student_row
|
||||
session_argument: hasura_session
|
||||
|
@ -0,0 +1,11 @@
|
||||
type: create_remote_relationship
|
||||
args:
|
||||
name: grade_offset
|
||||
table: students
|
||||
hasura_fields:
|
||||
- total_marks_offset
|
||||
remote_schema: my-remote-schema
|
||||
remote_field:
|
||||
getGrade:
|
||||
arguments:
|
||||
marks: "$total_marks_offset"
|
@ -0,0 +1,25 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: create_remote_relationship
|
||||
args:
|
||||
name: grade
|
||||
table: students
|
||||
hasura_fields:
|
||||
- total_marks
|
||||
remote_schema: my-remote-schema
|
||||
remote_field:
|
||||
getGrade:
|
||||
arguments:
|
||||
marks: "$total_marks"
|
||||
|
||||
- type: create_remote_relationship
|
||||
args:
|
||||
name: grade_session
|
||||
table: students
|
||||
hasura_fields:
|
||||
- total_marks_session
|
||||
remote_schema: my-remote-schema
|
||||
remote_field:
|
||||
getGrade:
|
||||
arguments:
|
||||
marks: "$total_marks_session"
|
@ -7,6 +7,10 @@ args:
|
||||
drop table if exists user_profiles;
|
||||
drop table if exists authors;
|
||||
drop table if exists employees;
|
||||
drop function if exists total_marks(students);
|
||||
drop function if exists total_marks_offset(students, integer);
|
||||
drop function if exists total_marks_session(students, json);
|
||||
drop table if exists students;
|
||||
|
||||
# also drops remote relationship as direct dep
|
||||
- type: remove_remote_schema
|
||||
|
@ -79,6 +79,7 @@ const typeDefs = gql`
|
||||
communications(id: Int): [Communication]
|
||||
search(id: Int!): SearchResult
|
||||
getOccupation(name: ID!): Occupation!
|
||||
getGrade(marks: Int!): String
|
||||
}
|
||||
`;
|
||||
|
||||
@ -224,6 +225,17 @@ const resolvers = {
|
||||
default:
|
||||
throw new ApolloError("invalid argument - " + name, "invalid ");
|
||||
}
|
||||
},
|
||||
getGrade: (_, { marks }) => {
|
||||
if(marks > 90) {
|
||||
return 'S'
|
||||
}
|
||||
else if (marks > 80) {
|
||||
return 'A'
|
||||
}
|
||||
else {
|
||||
return 'B'
|
||||
}
|
||||
}
|
||||
},
|
||||
Communication: {
|
||||
|
@ -66,6 +66,9 @@ class TestCreateRemoteRelationship:
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_with_enum.yaml')
|
||||
assert st_code == 200, resp
|
||||
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_computed_fields.yaml')
|
||||
assert st_code == 200, resp
|
||||
|
||||
def test_create_invalid(self, hge_ctx):
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_hasura_field.yaml')
|
||||
assert st_code == 400, resp
|
||||
@ -94,6 +97,9 @@ class TestCreateRemoteRelationship:
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_array.yaml')
|
||||
assert st_code == 400, resp
|
||||
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_computed_field.yaml')
|
||||
assert st_code == 400, resp
|
||||
|
||||
def test_generation(self, hge_ctx):
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_basic.yaml')
|
||||
assert st_code == 200, resp
|
||||
@ -361,3 +367,26 @@ class TestWithRelay:
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_basic.yaml')
|
||||
assert st_code == 200, resp
|
||||
check_query_f(hge_ctx, self.dir() + "with_relay.yaml")
|
||||
|
||||
class TestComputedFieldsInRemoteRelationship:
|
||||
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return "queries/remote_schemas/remote_relationships/"
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, hge_ctx, graphql_service):
|
||||
print("In setup method")
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup.yaml')
|
||||
assert st_code == 200, resp
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_computed_fields.yaml')
|
||||
assert st_code == 200, resp
|
||||
yield
|
||||
st_code, resp = hge_ctx.v1q_f(self.dir() + 'teardown.yaml')
|
||||
assert st_code == 200, resp
|
||||
|
||||
def test_remote_join_with_computed_field(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + 'remote_join_with_computed_field.yaml')
|
||||
|
||||
def test_remote_join_with_computed_field_session(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + 'remote_join_with_computed_field_session.yaml')
|
||||
|
Loading…
Reference in New Issue
Block a user