-- | A collection of types and utilities around the @Node@ GraphQL
-- type exposed by the Relay API.
module Hasura.GraphQL.Schema.Node
  ( -- * Node id
    NodeId (..),
    V1NodeId (..),
    V2NodeId (..),

    -- * Node id version
    NodeIdVersion,
    nodeIdVersionInt,
    currentNodeIdVersion,

    -- * Internal relay types
    NodeMap,
    TableMap (..),
    NodeInfo (..),
    findNode,
  )
where

import Data.Aeson qualified as J
import Data.Aeson.Types qualified as J
import Data.HashMap.Strict qualified as Map
import Data.Sequence qualified as Seq
import Data.Sequence.NonEmpty qualified as NESeq
import Hasura.Backends.Postgres.SQL.Types qualified as PG
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Table
import Hasura.SQL.AnyBackend qualified as AB

{- Note [Relay Node Id]
~~~~~~~~~~~~~~~~~~~~~~~

Relay API
---------

The 'Node' interface in the Relay API schema has exactly one field, which
returns a non-null 'ID' value. In a backend that supports the Relay API, each
table's corresponding GraphQL object implements that interface, and provides an
@id@ field that uniuqely identifies each row of the table. See
https://relay.dev/graphql/objectidentification.htm for more details.

To uniquely identify a given row in a given table, we use two different pieces
of information:
  - something that uniquely identifies the table within the schema
  - something that uniquely identifies the row within the table

Both V1 and V2 (of this particular API, not of the engine, see 'NodeIdVersion')
use the same data to uniquely identify the row within the table: a list of
values that map to the table's primary keys, in order. Where they differentiate
is on how they identify the table within the schema:
  - V1 only used a Postgres table name;
  - V2 uses a source name, and a backend-agnostic table name

For now, we still only emit and accept V1 ids: switching to emitting V2 node ids
will be a breaking change that will we do soon. We will continue to accept V1
node ids after that change, meaning we still to resolve them; in practice, that
means iterating over all the Postgres sources, until we find one that has a
table with the given name. If we find more than one, then we fail, to avoid
having to pick a random one (and potentially silently return wrong results.)

Id format
---------

All the required information is encoded into a unique node id using the
following pipeline:

    values <-> JSON array <-> bytestring <-> base64 string

In v1, the content of the JSON array was:

    [ 1         -- JSON number: version number
    , "public"  -- JSON string: Postgres schema name
    , "foo"     -- JSON string: Postgres table name
    , ...       -- arbitrary JSON values: values for each primary key, in order
    ]

As of v2, the content of the JSON array is as follows:

    [ 2                    -- JSON number: version number
    , "default"            -- JSON string: source name
    , "postgres"           -- JSON string: backend type
    , { "schema: "public"  -- arbitrary JSON value: table name in that backend
      , "name": "foo"
      }
    , ...                  -- arbitrary JSON values: values for each primary key, in order
    ]

Encoding and decoding
---------------------

The encoding of a given row's id is performed in each backend's translation
layer, as crafting the row's id requires extracting information out of the
database (the primary key values). Selecting the 'id' field of a compatible
table will yield an 'AFNodeId' field in the IR (see Hasura.RQL.IR.Select), that
each compatible backend will then interpret appropriately.

Decoding, however, does not require introspecting the database, and is performed
at parsing time, so that we can select the corresponing table row. See
'nodeField' in 'Relay.hs' for more information.
-}

--------------------------------------------------------------------------------
-- Node id

data NodeId
  = NodeIdV1 V1NodeId
  | NodeIdV2 (AB.AnyBackend V2NodeId)

-- | V1 format of a node.
--
-- This id does NOT uniquely identify the table properly, as it only knows the
-- table's name, but doesn't store a source name.
data V1NodeId = V1NodeId
  { V1NodeId -> QualifiedTable
_ni1Table :: PG.QualifiedTable,
    V1NodeId -> NESeq Value
_ni1Columns :: NESeq.NESeq J.Value
  }

-- | V2 format of a node.
--
-- Uniquely identifies a table with source name and table name, and uniquely
-- identifies a row within that table with a list of primary key values.
data V2NodeId b = V2NodeId
  { V2NodeId b -> SourceName
_ni2Source :: SourceName,
    V2NodeId b -> TableName b
_ni2Table :: TableName b,
    V2NodeId b -> NESeq Value
_ni2Columns :: NESeq.NESeq J.Value
  }

instance J.FromJSON NodeId where
  parseJSON :: Value -> Parser NodeId
parseJSON = String -> (Array -> Parser NodeId) -> Value -> Parser NodeId
forall a. String -> (Array -> Parser a) -> Value -> Parser a
J.withArray String
"node id" \Array
array -> case Array -> [Value]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList Array
array of
    [] -> String -> Parser NodeId
forall (m :: * -> *) a. MonadFail m => String -> m a
fail String
"unexpected GUID format, found empty list"
    J.Number Scientific
1 : [Value]
rest -> V1NodeId -> NodeId
NodeIdV1 (V1NodeId -> NodeId) -> Parser V1NodeId -> Parser NodeId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Value] -> Parser V1NodeId
parseNodeIdV1 [Value]
rest
    J.Number Scientific
n : [Value]
_ -> String -> Parser NodeId
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (String -> Parser NodeId) -> String -> Parser NodeId
forall a b. (a -> b) -> a -> b
$ String
"unsupported GUID version: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Scientific -> String
forall a. Show a => a -> String
show Scientific
n
    [Value]
_ -> String -> Parser NodeId
forall (m :: * -> *) a. MonadFail m => String -> m a
fail String
"unexpected GUID format, needs to start with a version number"

parseNodeIdV1 :: [J.Value] -> J.Parser V1NodeId
parseNodeIdV1 :: [Value] -> Parser V1NodeId
parseNodeIdV1 (Value
schemaValue : Value
nameValue : Value
firstColumn : [Value]
remainingColumns) =
  QualifiedTable -> NESeq Value -> V1NodeId
V1NodeId
    (QualifiedTable -> NESeq Value -> V1NodeId)
-> Parser QualifiedTable -> Parser (NESeq Value -> V1NodeId)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (SchemaName -> TableName -> QualifiedTable
forall a. SchemaName -> a -> QualifiedObject a
PG.QualifiedObject (SchemaName -> TableName -> QualifiedTable)
-> Parser SchemaName -> Parser (TableName -> QualifiedTable)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Value -> Parser SchemaName
forall a. FromJSON a => Value -> Parser a
J.parseJSON Value
schemaValue Parser (TableName -> QualifiedTable)
-> Parser TableName -> Parser QualifiedTable
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Value -> Parser TableName
forall a. FromJSON a => Value -> Parser a
J.parseJSON Value
nameValue)
    Parser (NESeq Value -> V1NodeId)
-> Parser (NESeq Value) -> Parser V1NodeId
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> NESeq Value -> Parser (NESeq Value)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Value
firstColumn Value -> Seq Value -> NESeq Value
forall a. a -> Seq a -> NESeq a
NESeq.:<|| [Value] -> Seq Value
forall a. [a] -> Seq a
Seq.fromList [Value]
remainingColumns)
parseNodeIdV1 [Value]
_ = String -> Parser V1NodeId
forall (m :: * -> *) a. MonadFail m => String -> m a
fail String
"GUID version 1: expecting schema name, table name and at least one column value"

--------------------------------------------------------------------------------
-- Node id version

-- | Enum representing the supported versions of the API.
data NodeIdVersion
  = NIVersion1
  | NIVersion2
  deriving (Int -> NodeIdVersion -> String -> String
[NodeIdVersion] -> String -> String
NodeIdVersion -> String
(Int -> NodeIdVersion -> String -> String)
-> (NodeIdVersion -> String)
-> ([NodeIdVersion] -> String -> String)
-> Show NodeIdVersion
forall a.
(Int -> a -> String -> String)
-> (a -> String) -> ([a] -> String -> String) -> Show a
showList :: [NodeIdVersion] -> String -> String
$cshowList :: [NodeIdVersion] -> String -> String
show :: NodeIdVersion -> String
$cshow :: NodeIdVersion -> String
showsPrec :: Int -> NodeIdVersion -> String -> String
$cshowsPrec :: Int -> NodeIdVersion -> String -> String
Show, NodeIdVersion -> NodeIdVersion -> Bool
(NodeIdVersion -> NodeIdVersion -> Bool)
-> (NodeIdVersion -> NodeIdVersion -> Bool) -> Eq NodeIdVersion
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: NodeIdVersion -> NodeIdVersion -> Bool
$c/= :: NodeIdVersion -> NodeIdVersion -> Bool
== :: NodeIdVersion -> NodeIdVersion -> Bool
$c== :: NodeIdVersion -> NodeIdVersion -> Bool
Eq)

nodeIdVersionInt :: NodeIdVersion -> Int
nodeIdVersionInt :: NodeIdVersion -> Int
nodeIdVersionInt = \case
  NodeIdVersion
NIVersion1 -> Int
1
  NodeIdVersion
NIVersion2 -> Int
2

currentNodeIdVersion :: NodeIdVersion
currentNodeIdVersion :: NodeIdVersion
currentNodeIdVersion = NodeIdVersion
NIVersion1

--------------------------------------------------------------------------------
-- Internal relay types

{- Note [Internal Relay HashMap]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Parsing the Node interface
--------------------------

When selecting a node in the schema, the user can use GraphQL fragments to
select different rows based on what table the node id maps to. For instance, a
Relay query could look like this (assuming that there are corresponding tables
"album" and "track" in the schema, possibly in different sources):

    query {
      node(id: "base64idhere") {
        ... on album {
          albumName
        }
        ... on track {
          trackName
        }
      }
    }

What that means is that the parser for the 'Node' interface needs to delegate to
*every table parser*, to deal with all possible cases. In practice, we use the
'selectionSetInterface' combinator (from Hasura.GraphQL.Parser.Internal.Parser):
we give it a list of all the parsers, and it in turn applies all of them, and
gives us the result for each possible table:
  - if the table was "album", the parsed result is: ...
  - if the table was "track", the parsed result is: ...

The parser for the interface itself cannot know what the actual underlying table
is: that's determined by the node id, which is not something inherent to the
interface! Consequently, what the parser for the interface returns is a
container, that to every supported table in the schema, associates the
corresponding parser output; the node *field* can then use that map and the node
id it got as an argument to extract the relevant information out of said
container.

The 'NodeMap' container
-----------------------

To avoid having to do extra lookups, we also store in that container additional
information about the table: permissions for the current role, connection
information... so that the field, by simply doing a lookup based on the node id,
can have all the information it needs to craft a corresponding query.

In practice: the value we store in our container is a 'NodeInfo' (see
below). Our container, in turn, isn't a 'HashMap' from "unique table identifier"
to 'NodeInfo'; the problem is that not all sources have the same backend type,
meaning that the "unique table identifier" would need to be a _hetereogeneous_
key type. This can be achieved with a dependent map (such as
Data.Dependent.Map.DMap), but is extremely cumbersome. Instead, our overall
container, 'NodeMap', is two layers of 'HashMap': to a source name, we associate
a "backend-erased" 'TableMap' which, in turn, for the corresponding backend,
associates to a table name the corresponding 'NodeInfo'.

Module structure
----------------

Ideally, none of those types should be exported: they are used in the return
type of 'nodeInteface', but consumed immediately by 'nodeField' (see both in
Relay.hs), and they could therefore be purely internal... except for the fact
that 'Common.hs' needs to know about the NodeMap, which is why it is defined
here instead of being an implementation detail of 'Relay.hs'.
-}

type NodeMap = HashMap SourceName (AB.AnyBackend TableMap)

-- | All the information required to craft a query to a row pointed to by a
-- 'NodeId'.
data NodeInfo b = NodeInfo
  { NodeInfo b -> SourceConfig b
nvSourceConfig :: SourceConfig b,
    NodeInfo b -> SelPermInfo b
nvSelectPermissions :: SelPermInfo b,
    NodeInfo b -> PrimaryKeyColumns b
nvPrimaryKeys :: PrimaryKeyColumns b,
    NodeInfo b
-> AnnFieldsG
     b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b)
nvAnnotatedFields :: IR.AnnFieldsG b (IR.RemoteRelationshipField IR.UnpreparedValue) (IR.UnpreparedValue b)
  }

newtype TableMap b = TableMap (HashMap (TableName b) (NodeInfo b))

-- | Given a source name and table name, peform the double lookup within a
-- 'NodeMap'.
findNode :: forall b. Backend b => SourceName -> TableName b -> NodeMap -> Maybe (NodeInfo b)
findNode :: SourceName -> TableName b -> NodeMap -> Maybe (NodeInfo b)
findNode SourceName
sourceName TableName b
tableName NodeMap
nodeMap = do
  AnyBackend TableMap
anyTableMap <- SourceName -> NodeMap -> Maybe (AnyBackend TableMap)
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
Map.lookup SourceName
sourceName NodeMap
nodeMap
  TableMap HashMap (TableName b) (NodeInfo b)
tableMap <- AnyBackend TableMap -> Maybe (TableMap b)
forall (b :: BackendType) (i :: BackendType -> *).
HasTag b =>
AnyBackend i -> Maybe (i b)
AB.unpackAnyBackend @b AnyBackend TableMap
anyTableMap
  TableName b
-> HashMap (TableName b) (NodeInfo b) -> Maybe (NodeInfo b)
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
Map.lookup TableName b
tableName HashMap (TableName b) (NodeInfo b)
tableMap