Support nested object fields in DC API and use this to implement nest…

## Description

This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent.

### Data Connector API changes

- The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name.
- Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field.

### MongoDB agent changes

- Add support for querying into nested documents using the new `object` field type.

### HGE changes

- The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend.
- For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`.
- If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source.
- During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`.
- When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`.
- The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent.

## Limitations

### HGE not yet implemented:
- Support for nested arrays
- Support for nested objects/arrays in mutations
- Support for nested objects/arrays in order-by
- Support for filters (`where`) in nested objects/arrays
- Support for adding custom GraphQL types via track table metadata API
- Support for interface and union types
- Tests for nested objects

### Mongo agent not yet implemented:

- Generate nested object types from validation schema
- Support for aggregates
- Support for order-by
- Configure agent port
- Build agent in CI
- Agent tests for nested objects and MongoDB agent

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844
GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
This commit is contained in:
David Overton 2023-04-11 11:29:05 +10:00 committed by hasura-bot
parent a1cd2b8176
commit 346804fc67
46 changed files with 595 additions and 89 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@hasura/dc-api-types",
"version": "0.29.0",
"version": "0.30.0",
"description": "Hasura GraphQL Engine Data Connector Agent API types",
"author": "Hasura (https://github.com/hasura/graphql-engine)",
"license": "Apache-2.0",

View File

@ -1035,6 +1035,13 @@
},
"SchemaResponse": {
"properties": {
"objectTypes": {
"description": "Object type definitions referenced in this schema",
"items": {
"$ref": "#/components/schemas/ObjectTypeDefinition"
},
"type": "array"
},
"tables": {
"description": "Available tables",
"items": {
@ -1241,6 +1248,30 @@
],
"type": "object"
},
"ObjectTypeDefinition": {
"properties": {
"columns": {
"description": "The columns of the type",
"items": {
"$ref": "#/components/schemas/ColumnInfo"
},
"type": "array"
},
"description": {
"description": "The description of the type",
"type": "string"
},
"name": {
"description": "The name of the type",
"type": "string"
}
},
"required": [
"name",
"columns"
],
"type": "object"
},
"QueryResponse": {
"properties": {
"aggregates": {
@ -1413,6 +1444,28 @@
},
"type": "object"
},
"NestedObjectField": {
"properties": {
"column": {
"type": "string"
},
"query": {
"$ref": "#/components/schemas/Query"
},
"type": {
"enum": [
"object"
],
"type": "string"
}
},
"required": [
"column",
"query",
"type"
],
"type": "object"
},
"RelationshipField": {
"properties": {
"query": {
@ -1462,11 +1515,15 @@
"discriminator": {
"mapping": {
"column": "ColumnField",
"object": "NestedObjectField",
"relationship": "RelationshipField"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/components/schemas/NestedObjectField"
},
{
"$ref": "#/components/schemas/RelationshipField"
},

View File

@ -63,12 +63,14 @@ export type { MutationOperation } from './models/MutationOperation';
export type { MutationOperationResults } from './models/MutationOperationResults';
export type { MutationRequest } from './models/MutationRequest';
export type { MutationResponse } from './models/MutationResponse';
export type { NestedObjectField } from './models/NestedObjectField';
export type { NotExpression } from './models/NotExpression';
export type { NullColumnFieldValue } from './models/NullColumnFieldValue';
export type { NullColumnInsertFieldValue } from './models/NullColumnInsertFieldValue';
export type { ObjectRelationInsertFieldValue } from './models/ObjectRelationInsertFieldValue';
export type { ObjectRelationInsertionOrder } from './models/ObjectRelationInsertionOrder';
export type { ObjectRelationInsertSchema } from './models/ObjectRelationInsertSchema';
export type { ObjectTypeDefinition } from './models/ObjectTypeDefinition';
export type { OpenApiDiscriminator } from './models/OpenApiDiscriminator';
export type { OpenApiExternalDocumentation } from './models/OpenApiExternalDocumentation';
export type { OpenApiReference } from './models/OpenApiReference';

View File

@ -3,7 +3,8 @@
/* eslint-disable */
import type { ColumnField } from './ColumnField';
import type { NestedObjectField } from './NestedObjectField';
import type { RelationshipField } from './RelationshipField';
export type Field = (RelationshipField | ColumnField);
export type Field = (NestedObjectField | RelationshipField | ColumnField);

View File

@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Query } from './Query';
export type NestedObjectField = {
column: string;
query: Query;
type: 'object';
};

View File

@ -0,0 +1,21 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ColumnInfo } from './ColumnInfo';
export type ObjectTypeDefinition = {
/**
* The columns of the type
*/
columns: Array<ColumnInfo>;
/**
* The description of the type
*/
description?: string;
/**
* The name of the type
*/
name: string;
};

View File

@ -2,9 +2,14 @@
/* tslint:disable */
/* eslint-disable */
import type { ObjectTypeDefinition } from './ObjectTypeDefinition';
import type { TableInfo } from './TableInfo';
export type SchemaResponse = {
/**
* Object type definitions referenced in this schema
*/
objectTypes?: Array<ObjectTypeDefinition>;
/**
* Available tables
*/

View File

@ -24,7 +24,7 @@
},
"dc-api-types": {
"name": "@hasura/dc-api-types",
"version": "0.29.0",
"version": "0.30.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
@ -2227,7 +2227,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify": "^4.13.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",
@ -2547,7 +2547,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify": "^4.13.0",
"fastify-metrics": "^9.2.1",
"nanoid": "^3.3.4",
@ -2868,7 +2868,7 @@
"version": "file:reference",
"requires": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"@tsconfig/node16": "^1.0.3",
"@types/node": "^16.11.49",
"@types/xml2js": "^0.4.11",
@ -3080,7 +3080,7 @@
"version": "file:sqlite",
"requires": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"@tsconfig/node16": "^1.0.3",
"@types/node": "^16.11.49",
"@types/sqlite3": "^3.1.8",

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify": "^4.13.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",
@ -52,7 +52,7 @@
"integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ=="
},
"node_modules/@hasura/dc-api-types": {
"version": "0.29.0",
"version": "0.30.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify": "^4.13.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",

View File

@ -452,6 +452,9 @@ const projectRow = (fields: Record<string, Field>, findRelationship: (relationsh
projectedRow[fieldName] = subquery ? performQuery(relationship.target_table, subquery) : { aggregates: null, rows: null };
break;
case "object":
throw new Error('Unsupported field type "object"');
default:
return unreachable(field["type"]);
}

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify": "^4.13.0",
"fastify-metrics": "^9.2.1",
"nanoid": "^3.3.4",
@ -57,7 +57,7 @@
"integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ=="
},
"node_modules/@hasura/dc-api-types": {
"version": "0.29.0",
"version": "0.30.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.29.0",
"@hasura/dc-api-types": "0.30.0",
"fastify-metrics": "^9.2.1",
"fastify": "^4.13.0",
"nanoid": "^3.3.4",

View File

@ -107,6 +107,8 @@ export function json_object(relationships: TableRelationships[], fields: Fields,
throw new Error(`Couldn't find relationship ${field.relationship} for field ${fieldName} on table ${table}`);
}
return `'${fieldName}', ${relationship(relationships, rel, field, tableAlias)}`;
case "object":
throw new Error('Unsupported field type "object"');
default:
return unreachable(field["type"]);
}

View File

@ -24,10 +24,15 @@ module Hasura.Backends.DataConnector.API.V0.Query
FieldValue,
mkColumnFieldValue,
mkRelationshipFieldValue,
mkNestedObjFieldValue,
mkNestedArrayFieldValue,
deserializeAsColumnFieldValue,
deserializeAsRelationshipFieldValue,
deserializeAsNestedObjFieldValue,
deserializeAsNestedArrayFieldValue,
_ColumnFieldValue,
_RelationshipFieldValue,
_NestedObjFieldValue,
)
where
@ -158,6 +163,7 @@ relationshipFieldObjectCodec =
data Field
= ColumnField API.V0.ColumnName API.V0.ScalarType
| RelField RelationshipField
| NestedObjField API.V0.ColumnName Query
deriving stock (Eq, Ord, Show, Generic)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Field
@ -169,15 +175,25 @@ instance HasCodec Field where
where
columnCodec =
(,)
<$> requiredField' "column" .= fst
<*> requiredField' "column_type" .= snd
<$> requiredField' "column"
.= fst
<*> requiredField' "column_type"
.= snd
nestedObjCodec =
(,)
<$> requiredField' "column"
.= fst
<*> requiredField' "query"
.= snd
enc = \case
ColumnField columnName scalarType -> ("column", mapToEncoder (columnName, scalarType) columnCodec)
RelField relField -> ("relationship", mapToEncoder relField relationshipFieldObjectCodec)
NestedObjField columnName nestedObjQuery -> ("object", mapToEncoder (columnName, nestedObjQuery) nestedObjCodec)
dec =
HashMap.fromList
[ ("column", ("ColumnField", mapToDecoder (uncurry ColumnField) columnCodec)),
("relationship", ("RelationshipField", mapToDecoder RelField relationshipFieldObjectCodec))
("relationship", ("RelationshipField", mapToDecoder RelField relationshipFieldObjectCodec)),
("object", ("NestedObjectField", mapToDecoder (uncurry NestedObjField) nestedObjCodec))
]
-- | The resolved query response provided by the 'POST /query'
@ -235,7 +251,8 @@ instance Ord FieldValue where
instance Show FieldValue where
showsPrec d fieldValue =
case deserializeFieldValueByGuessing fieldValue of
Left columnFieldValue -> showParen (d > appPrec) $ showString "ColumnFieldValue " . showsPrec appPrec1 columnFieldValue
Left (Left columnFieldValue) -> showParen (d > appPrec) $ showString "ColumnFieldValue " . showsPrec appPrec1 columnFieldValue
Left (Right nestedObjFieldValue) -> showParen (d > appPrec) $ showString "NestedObjFieldValue " . showsPrec appPrec1 nestedObjFieldValue
Right queryResponse -> showParen (d > appPrec) $ showString "RelationshipFieldValue " . showsPrec appPrec1 queryResponse
mkColumnFieldValue :: J.Value -> FieldValue
@ -244,6 +261,12 @@ mkColumnFieldValue = FieldValue
mkRelationshipFieldValue :: QueryResponse -> FieldValue
mkRelationshipFieldValue = FieldValue . J.toJSON
mkNestedObjFieldValue :: HashMap FieldName FieldValue -> FieldValue
mkNestedObjFieldValue = FieldValue . J.toJSON
mkNestedArrayFieldValue :: [FieldValue] -> FieldValue
mkNestedArrayFieldValue = FieldValue . J.toJSON
deserializeAsColumnFieldValue :: FieldValue -> J.Value
deserializeAsColumnFieldValue (FieldValue value) = value
@ -253,9 +276,26 @@ deserializeAsRelationshipFieldValue (FieldValue value) =
J.Error s -> Left $ T.pack s
J.Success queryResponse -> Right queryResponse
deserializeFieldValueByGuessing :: FieldValue -> Either J.Value QueryResponse
deserializeAsNestedObjFieldValue :: FieldValue -> Either Text (HashMap FieldName FieldValue)
deserializeAsNestedObjFieldValue (FieldValue value) =
case J.fromJSON value of
J.Error s -> Left $ T.pack s
J.Success obj -> Right obj
deserializeAsNestedArrayFieldValue :: FieldValue -> Either Text [FieldValue]
deserializeAsNestedArrayFieldValue (FieldValue value) =
case J.fromJSON value of
J.Error s -> Left $ T.pack s
J.Success obj -> Right obj
deserializeFieldValueByGuessing :: FieldValue -> (Either (Either (Either Value (HashMap FieldName FieldValue)) [FieldValue]) QueryResponse)
deserializeFieldValueByGuessing fieldValue =
left (const $ deserializeAsColumnFieldValue fieldValue) $ deserializeAsRelationshipFieldValue fieldValue
left
( const $
left (const $ left (const $ deserializeAsColumnFieldValue fieldValue) $ deserializeAsNestedObjFieldValue fieldValue) $
deserializeAsNestedArrayFieldValue fieldValue
)
$ deserializeAsRelationshipFieldValue fieldValue
-- | Even though we could just describe a FieldValue as "any JSON value", we're explicitly
-- describing it in terms of either a 'QueryResponse' or "any JSON value", in order to
@ -288,6 +328,12 @@ _ColumnFieldValue = lens deserializeAsColumnFieldValue (const mkColumnFieldValue
_RelationshipFieldValue :: Prism' FieldValue QueryResponse
_RelationshipFieldValue = prism' mkRelationshipFieldValue (either (const Nothing) Just . deserializeAsRelationshipFieldValue)
_NestedObjFieldValue :: Prism' FieldValue (HashMap FieldName FieldValue)
_NestedObjFieldValue = prism' mkNestedObjFieldValue (either (const Nothing) Just . deserializeAsNestedObjFieldValue)
_NestedArrayFieldValue :: Prism' FieldValue [FieldValue]
_NestedArrayFieldValue = prism' mkNestedArrayFieldValue (either (const Nothing) Just . deserializeAsNestedArrayFieldValue)
$(makeLenses ''QueryRequest)
$(makeLenses ''Query)
$(makeLenses ''QueryResponse)

View File

@ -2,6 +2,7 @@
module Hasura.Backends.DataConnector.API.V0.Schema
( SchemaResponse (..),
ObjectTypeDefinition (..),
)
where
@ -9,9 +10,13 @@ import Autodocodec
import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, ToJSON)
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty)
import Data.OpenApi (ToSchema)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0
import Language.GraphQL.Draft.Syntax qualified as G
import Servant.API qualified as Servant
import Prelude
@ -20,17 +25,37 @@ import Prelude
-- | The Schema Response provides the schemas for tracked tables and
-- 'Capabilities' supported by the service.
newtype SchemaResponse = SchemaResponse
{ _srTables :: [API.V0.TableInfo]
data SchemaResponse = SchemaResponse
{ _srTables :: [API.V0.TableInfo],
_srObjectTypes :: Maybe (NonEmpty ObjectTypeDefinition)
}
deriving stock (Eq, Ord, Show, Generic)
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec SchemaResponse
instance HasCodec SchemaResponse where
codec =
object "SchemaResponse" $
SchemaResponse <$> requiredField "tables" "Available tables" .= _srTables
SchemaResponse
<$> requiredField "tables" "Available tables" .= _srTables
<*> optionalField "objectTypes" "Object type definitions referenced in this schema" .= _srObjectTypes
instance Servant.HasStatus SchemaResponse where
type StatusOf SchemaResponse = 200
data ObjectTypeDefinition = ObjectTypeDefinition
{ _otdName :: G.Name,
_otdDescription :: Maybe Text,
_otdColumns :: NonEmpty API.V0.ColumnInfo
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec ObjectTypeDefinition
instance HasCodec ObjectTypeDefinition where
codec =
object "ObjectTypeDefinition" $
ObjectTypeDefinition
<$> requiredField "name" "The name of the type" .= _otdName
<*> optionalField "description" "The description of the type" .= _otdDescription
<*> requiredField "columns" "The columns of the type" .= _otdColumns

View File

@ -77,7 +77,16 @@ schemaTables :: [API.TableInfo]
schemaTables = either error id . eitherDecodeStrict $ schemaBS
numericColumns :: [API.ColumnName]
numericColumns = schemaTables >>= (API._tiColumns >>> mapMaybe (\API.ColumnInfo {..} -> if _ciType == API.ScalarType "number" then Just _ciName else Nothing))
numericColumns =
schemaTables
>>= ( API._tiColumns
>>> mapMaybe
( \API.ColumnInfo {..} ->
if _ciType == API.ScalarType "number"
then Just _ciName
else Nothing
)
)
edgeCasesSchemaBS :: ByteString
edgeCasesSchemaBS = $(makeRelativeToProject "test/Test/Data/edge-cases-schema-tables.json" >>= embedFile)

View File

@ -826,7 +826,8 @@ schema =
API._tiUpdatable = True,
API._tiDeletable = True
}
]
],
_srObjectTypes = Nothing
}
-- | Stock 'MockConfig' for a Chinook Agent.

View File

@ -132,7 +132,8 @@ resolveSource sourceConfig =
_ptmiForeignKeys = mempty,
_ptmiViewInfo = Just $ ViewInfo False False False,
_ptmiDescription = Nothing,
_ptmiExtraTableMetadata = ()
_ptmiExtraTableMetadata = (),
_ptmiCustomObjectTypes = mempty
}
)
| (index, RestTable {tableReference, schema}) <-

View File

@ -77,6 +77,7 @@ instance Backend 'DataConnector where
type XEventTriggers 'DataConnector = XDisable
type XNestedInserts 'DataConnector = XDisable
type XStreamingSubscription 'DataConnector = XDisable
type XNestedObjects 'DataConnector = XEnable
type HealthCheckTest 'DataConnector = Void

View File

@ -35,6 +35,7 @@ import Hasura.RQL.IR.BoolExp (OpExpG (..), PartialSQLExp (..), RootOrCurrent (..
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Column qualified as RQL.T.C
import Hasura.RQL.Types.Common (OID (..), SourceName)
import Hasura.RQL.Types.CustomTypes (GraphQLType (..))
import Hasura.RQL.Types.EventTrigger (RecreateEventTriggers (RETDoNothing))
import Hasura.RQL.Types.Metadata (BackendConfigWrapper, SourceMetadata (..))
import Hasura.RQL.Types.Metadata qualified as Metadata
@ -55,6 +56,7 @@ import Hasura.Services.Network
import Hasura.Session (SessionVariable, mkSessionVariable)
import Hasura.Tracing (ignoreTraceT)
import Hasura.Tracing qualified as Tracing
import Language.GraphQL.Draft.Syntax qualified as G
import Language.GraphQL.Draft.Syntax qualified as GQL
import Network.HTTP.Client qualified as HTTP
import Servant.API (Union)
@ -81,6 +83,7 @@ instance BackendMetadata 'DataConnector where
postDropSourceHook _sourceConfig _tableTriggerMap = pure ()
buildComputedFieldBooleanExp _ _ _ _ _ _ =
error "buildComputedFieldBooleanExp: not implemented for the Data Connector backend."
columnInfoToFieldInfo = columnInfoToFieldInfo'
listAllTables = listAllTables'
supportsBeingRemoteRelationshipTarget = supportsBeingRemoteRelationshipTarget'
@ -192,7 +195,10 @@ resolveDatabaseMetadata' ::
DC.SourceConfig ->
m (Either QErr (DBObjectsIntrospection 'DataConnector))
resolveDatabaseMetadata' _ DC.SourceConfig {_scSchema = API.SchemaResponse {..}, ..} =
let tables = HashMap.fromList $ do
let typeNames = maybe mempty (HashSet.fromList . toList . fmap API._otdName) _srObjectTypes
customObjectTypes =
maybe mempty (HashMap.fromList . mapMaybe (toTableObjectType _scCapabilities typeNames) . toList) _srObjectTypes
tables = HashMap.fromList $ do
API.TableInfo {..} <- _srTables
let primaryKeyColumns = fmap Witch.from . NESeq.fromList <$> _tiPrimaryKey
let meta =
@ -224,7 +230,8 @@ resolveDatabaseMetadata' _ DC.SourceConfig {_scSchema = API.SchemaResponse {..},
_tiColumns
& fmap (\API.ColumnInfo {..} -> (Witch.from _ciName, DC.ExtraColumnMetadata _ciValueGenerated))
& HashMap.fromList
}
},
_ptmiCustomObjectTypes = Just customObjectTypes
}
pure (coerce _tiName, meta)
in pure $
@ -235,6 +242,25 @@ resolveDatabaseMetadata' _ DC.SourceConfig {_scSchema = API.SchemaResponse {..},
_rsScalars = mempty
}
toTableObjectType :: API.Capabilities -> HashSet G.Name -> API.ObjectTypeDefinition -> Maybe (G.Name, RQL.T.T.TableObjectType 'DataConnector)
toTableObjectType capabilities typeNames API.ObjectTypeDefinition {..} =
(_otdName,) . RQL.T.T.TableObjectType _otdName (G.Description <$> _otdDescription) <$> traverse toTableObjectFieldDefinition _otdColumns
where
toTableObjectFieldDefinition API.ColumnInfo {..} = do
fieldTypeName <- G.mkName $ API.getScalarType _ciType
fieldName <- G.mkName $ API.unColumnName _ciName
pure $
RQL.T.T.TableObjectFieldDefinition
{ _tofdColumn = Witch.from _ciName,
_tofdName = fieldName,
_tofdDescription = G.Description <$> _ciDescription,
_tofdGType = GraphQLType $ G.TypeNamed (G.Nullability _ciNullable) fieldTypeName,
_tofdFieldType =
if HashSet.member fieldTypeName typeNames
then RQL.T.T.TOFTObject fieldTypeName
else RQL.T.T.TOFTScalar fieldTypeName $ DC.mkScalarType capabilities _ciType
}
-- | Construct a 'HashSet' 'RQL.T.T.ForeignKeyMetadata'
-- 'DataConnector' to build the foreign key constraints in the table
-- metadata.
@ -390,6 +416,30 @@ mkTypedSessionVar columnType =
errorAction :: MonadError QErr m => API.ErrorResponse -> m a
errorAction e = throw400WithDetail DataConnectorError (errorResponseSummary e) (_crDetails e)
-- | This function assumes that if a type name is present in the custom object types for the table then it
-- refers to a nested object of that type.
-- Otherwise it is a normal (scalar) column.
columnInfoToFieldInfo' :: HashMap G.Name (RQL.T.T.TableObjectType 'DataConnector) -> RQL.T.C.ColumnInfo 'DataConnector -> RQL.T.T.FieldInfo 'DataConnector
columnInfoToFieldInfo' gqlTypes columnInfo@RQL.T.C.ColumnInfo {..} =
maybe (RQL.T.T.FIColumn columnInfo) RQL.T.T.FINestedObject getNestedObjectInfo
where
getNestedObjectInfo =
case ciType of
RQL.T.C.ColumnScalar (DC.ScalarType scalarTypeName _) -> do
gqlName <- GQL.mkName scalarTypeName
guard $ HashMap.member gqlName gqlTypes
pure $
RQL.T.C.NestedObjectInfo
{ RQL.T.C._noiSupportsNestedObjects = (),
RQL.T.C._noiColumn = ciColumn,
RQL.T.C._noiName = ciName,
RQL.T.C._noiType = gqlName,
RQL.T.C._noiIsNullable = ciIsNullable,
RQL.T.C._noiDescription = ciDescription,
RQL.T.C._noiMutability = ciMutability
}
RQL.T.C.ColumnEnumReference {} -> Nothing
supportsBeingRemoteRelationshipTarget' :: DC.SourceConfig -> Bool
supportsBeingRemoteRelationshipTarget' DC.SourceConfig {..} =
isJust $ API._qcForeach =<< API._cQueries _scCapabilities

View File

@ -326,6 +326,9 @@ translateAnnField sessionVariables sourceTableName = \case
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure Nothing
AFNestedObject nestedObj ->
Just . API.NestedObjField (Witch.from $ _anosColumn nestedObj)
<$> translateNestedObjectSelect sessionVariables sourceTableName nestedObj
translateArrayRelationSelect ::
( Has TableRelationships writerOutput,
@ -433,6 +436,28 @@ translateSingleColumnAggregateFunction functionName =
fmap API.SingleColumnAggregateFunction (G.mkName functionName)
`onNothing` throw500 ("translateSingleColumnAggregateFunction: Invalid aggregate function encountered: " <> functionName)
translateNestedObjectSelect ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m
) =>
SessionVariables ->
API.TableName ->
AnnNestedObjectSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m API.Query
translateNestedObjectSelect sessionVariables tableName selectG = do
FieldsAndAggregates {..} <- translateAnnFieldsWithNoAggregates sessionVariables noPrefix tableName $ _anosFields selectG
pure
API.Query
{ _qFields = mapFieldNameHashMap <$> _faaFields,
_qAggregates = Nothing,
_qAggregatesLimit = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Nothing,
_qOrderBy = Nothing
}
--------------------------------------------------------------------------------
-- | Validate if a 'API.QueryRequest' contains any relationships.
@ -564,6 +589,12 @@ reshapeField field responseFieldValue =
AFArrayRelation (ASAggregate aggregateArrayRelationField) ->
reshapeAnnRelationSelect reshapeTableAggregateFields aggregateArrayRelationField =<< responseFieldValue
AFExpression txt -> pure $ JE.text txt
AFNestedObject nestedObj -> do
nestedObjectFieldValue <- API.deserializeAsNestedObjFieldValue <$> responseFieldValue
case nestedObjectFieldValue of
Left err -> throw500 $ "Expected object in field returned by Data Connector agent: " <> err -- TODO(dmoverton): Add pathing information for error clarity
Right nestedResponse ->
reshapeAnnFields noPrefix (_anosFields nestedObj) nestedResponse
reshapeAnnRelationSelect ::
MonadError QErr m =>

View File

@ -155,6 +155,7 @@ transformTable tableInfo =
Nothing -- no views, only tables
Nothing -- no description
identityColumns
mempty
)
transformColumn ::

View File

@ -101,7 +101,8 @@ mergeMetadata InformationSchema {..} =
else HS.empty,
_ptmiViewInfo = Nothing,
_ptmiDescription = Nothing,
_ptmiExtraTableMetadata = ()
_ptmiExtraTableMetadata = (),
_ptmiCustomObjectTypes = mempty
}
mergeDBTableMetadata :: DBTableMetadata 'MySQL -> DBTableMetadata 'MySQL -> DBTableMetadata 'MySQL
@ -114,7 +115,8 @@ mergeDBTableMetadata new existing =
_ptmiForeignKeys = _ptmiForeignKeys existing <> _ptmiForeignKeys new, -- union
_ptmiViewInfo = _ptmiViewInfo existing <|> _ptmiViewInfo new,
_ptmiDescription = _ptmiDescription existing <|> _ptmiDescription new,
_ptmiExtraTableMetadata = ()
_ptmiExtraTableMetadata = (),
_ptmiCustomObjectTypes = mempty
}
data InformationSchema = InformationSchema

View File

@ -249,6 +249,12 @@ transformObjectSelect ::
Collector (AnnObjectSelectG b Void (UnpreparedValue b))
transformObjectSelect = traverseOf aosFields transformAnnFields
transformNestedObjectSelect ::
Backend b =>
AnnNestedObjectSelectG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b) ->
Collector (AnnNestedObjectSelectG b Void (UnpreparedValue b))
transformNestedObjectSelect = traverseOf anosFields transformAnnFields
transformGraphQLField ::
GraphQLField (RemoteRelationshipField UnpreparedValue) var ->
Collector (GraphQLField Void var)
@ -325,6 +331,8 @@ transformAnnFields fields = do
remoteAnnPlaceholder,
Just $ createRemoteJoin (Map.intersection joinColumnAliases _rrsLHSJoinFields) _rrsRelationship
)
AFNestedObject nestedObj ->
(,Nothing) . AFNestedObject <$> transformNestedObjectSelect nestedObj
let transformedFields = (fmap . fmap) fst annotatedFields
remoteJoins =

View File

@ -41,7 +41,6 @@ import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.CustomTypes
import Hasura.RQL.Types.Relationships.Remote
import Hasura.RQL.Types.Table
import Hasura.SQL.AnyBackend qualified as AB
import Hasura.SQL.Backend
import Hasura.Session
@ -337,7 +336,7 @@ actionOutputFields outputType annotatedObject objectTypes = do
_rsfiType = _atrType,
_rsfiSource = _atrSource,
_rsfiSourceConfig = _atrSourceConfig,
_rsfiTable = tableInfoName _atrTableInfo,
_rsfiTable = _atrTableName,
_rsfiMapping = joinMapping
}
}

View File

@ -161,6 +161,7 @@ boolExpInternal gqlName fieldInfos description memoizeKey mkAggPredParser = do
-- Using remote relationship fields in boolean expressions is not supported.
FIRemoteRelationship _ -> empty
FINestedObject _ -> empty -- TODO(dmoverton)
-- |
-- > input type_bool_exp {

View File

@ -23,6 +23,7 @@ module Hasura.GraphQL.Schema.Common
ConnectionSelectExp,
AnnotatedActionField,
AnnotatedActionFields,
AnnotatedNestedObjectSelect,
EdgeFields,
Scenario (..),
SelectArgs,
@ -311,6 +312,8 @@ type AnnotatedActionFields = IR.ActionFieldsG (IR.RemoteRelationshipField IR.Unp
type AnnotatedActionField = IR.ActionFieldG (IR.RemoteRelationshipField IR.UnpreparedValue)
type AnnotatedNestedObjectSelect b = IR.AnnNestedObjectSelectG b (IR.RemoteRelationshipField IR.UnpreparedValue) (IR.UnpreparedValue b)
-------------------------------------------------------------------------------
data RemoteSchemaParser n = RemoteSchemaParser

View File

@ -211,6 +211,7 @@ tableFieldsInput tableInfo = do
mkFieldParser = \case
FIComputedField _ -> pure Nothing
FIRemoteRelationship _ -> pure Nothing
FINestedObject _ -> pure Nothing -- TODO(dmoverton)
FIColumn columnInfo -> do
if (_cmIsInsertable $ ciMutability columnInfo)
then mkColumnParser columnInfo

View File

@ -180,6 +180,7 @@ orderByExpInternal gqlName description tableFields memoizeKey = do
aggregationOrderBy
ReturnsOthers -> empty
FIRemoteRelationship _ -> empty
FINestedObject _ -> empty -- TODO(dmoverton)
-- | Corresponds to an object type for an order by.
--

View File

@ -49,7 +49,7 @@ import Hasura.Base.Error
import Hasura.Base.ErrorMessage (toErrorMessage)
import Hasura.CustomReturnType.Cache (CustomReturnTypeInfo (..))
import Hasura.GraphQL.Parser.Class
import Hasura.GraphQL.Parser.Internal.Parser qualified as P
import Hasura.GraphQL.Parser.Internal.Parser qualified as IP
import Hasura.GraphQL.Schema.Backend
import Hasura.GraphQL.Schema.BoolExp
import Hasura.GraphQL.Schema.Common
@ -75,6 +75,7 @@ import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.CustomTypes
import Hasura.RQL.Types.Metadata.Object
import Hasura.RQL.Types.Permission qualified as Permission
import Hasura.RQL.Types.Relationships.Local
@ -460,7 +461,7 @@ defaultTableSelectionSet tableInfo = runMaybeT do
<&> parsedSelectionsToFields IR.AFExpression
where
selectionSetObjectWithDirective name description parsers implementsInterfaces directives =
P.setParserDirectives directives $
IP.setParserDirectives directives $
P.selectionSetObject name description parsers implementsInterfaces
-- | List of table fields object.
@ -1276,6 +1277,8 @@ fieldSelection table tableInfo = \case
pure $!
P.selection fieldName (ciDescription columnInfo) pathArg field
<&> IR.mkAnnColumnField (ciColumn columnInfo) (ciType columnInfo) caseBoolExpUnpreparedValue
FINestedObject nestedObjectInfo ->
pure . fmap IR.AFNestedObject <$> nestedObjectFieldParser tableInfo nestedObjectInfo
FIRelationship relationshipInfo ->
concat . maybeToList <$> relationshipField table relationshipInfo
FIComputedField computedFieldInfo ->
@ -1295,6 +1298,58 @@ fieldSelection table tableInfo = \case
relationshipFields <- fromMaybe [] <$> remoteRelationshipField remoteFieldInfo
let lhsFields = _rfiLHS remoteFieldInfo
pure $ map (fmap (IR.AFRemote . IR.RemoteRelationshipSelect lhsFields)) relationshipFields
where
nestedObjectFieldParser :: TableInfo b -> NestedObjectInfo b -> SchemaT r m (FieldParser n (AnnotatedNestedObjectSelect b))
nestedObjectFieldParser TableInfo {..} NestedObjectInfo {..} = do
let customObjectTypes = _tciCustomObjectTypes _tiCoreInfo
case Map.lookup _noiType customObjectTypes of
Just objectType -> do
parser <- nestedObjectParser _noiSupportsNestedObjects customObjectTypes objectType _noiColumn _noiIsNullable
pure $ P.subselection_ _noiName _noiDescription parser
_ -> throw500 $ "fieldSelection: object type " <> _noiType <<> " not found"
nestedObjectParser ::
forall b r m n.
MonadBuildSchema b r m n =>
XNestedObjects b ->
HashMap G.Name (TableObjectType b) ->
TableObjectType b ->
Column b ->
Bool ->
SchemaT r m (P.Parser 'Output n (AnnotatedNestedObjectSelect b))
nestedObjectParser supportsNestedObjects objectTypes objectType column isNullable = do
allFieldParsers <- for (toList $ _totFields objectType) outputFieldParser
pure $
outputParserModifier isNullable $
P.selectionSet (_totName objectType) (_totDescription objectType) allFieldParsers
<&> IR.AnnNestedObjectSelectG supportsNestedObjects column . parsedSelectionsToFields IR.AFExpression
where
outputParserModifier True = P.nullableParser
outputParserModifier False = P.nonNullableParser
outputFieldParser ::
TableObjectFieldDefinition b ->
SchemaT r m (IP.FieldParser MetadataObjId n (IR.AnnFieldG b (IR.RemoteRelationshipField IR.UnpreparedValue) (IR.UnpreparedValue b)))
outputFieldParser (TableObjectFieldDefinition column' name description (GraphQLType gType) objectFieldType) =
P.memoizeOn 'nestedObjectParser (_totName objectType, name) do
case objectFieldType of
TOFTScalar fieldTypeName scalarType ->
wrapScalar scalarType $ customScalarParser fieldTypeName
TOFTObject objectName -> do
objectType' <- Map.lookup objectName objectTypes `onNothing` throw500 ("Custom type " <> objectName <<> " not found")
parser <- fmap (IR.AFNestedObject @b) <$> nestedObjectParser supportsNestedObjects objectTypes objectType' column' (G.isNullable gType)
pure $ P.subselection_ name description parser
where
wrapScalar scalarType parser =
pure $
P.wrapFieldParser gType (P.selection_ name description parser)
$> IR.mkAnnColumnField column' (ColumnScalar scalarType) Nothing Nothing
customScalarParser fieldTypeName =
let schemaType = P.TNamed P.NonNullable $ P.Definition fieldTypeName Nothing Nothing [] P.TIScalar
in P.Parser
{ pType = schemaType,
pParser = P.valueToJSON (P.toGraphQLType schemaType)
}
{- Note [Permission filter deduplication]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1471,7 +1526,7 @@ relationshipField table ri = runMaybeT do
_ -> pure Nullable
pure $
pure $
case nullable of { Nullable -> id; NotNullable -> P.nonNullableField } $
case nullable of { Nullable -> id; NotNullable -> IP.nonNullableField } $
P.subselection_ relFieldName desc selectionSetParser
<&> \fields ->
IR.AFObjectRelation $

View File

@ -229,6 +229,8 @@ tableSelectFields tableInfo = do
canBeSelected _ Nothing _ = pure False
canBeSelected _ (Just permissions) (FIColumn columnInfo) =
pure $! Map.member (ciColumn columnInfo) (spiCols permissions)
canBeSelected _ (Just permissions) (FINestedObject NestedObjectInfo {..}) =
pure $! Map.member _noiColumn (spiCols permissions)
canBeSelected role _ (FIRelationship relationshipInfo) = do
tableInfo' <- askTableInfo $ riRTable relationshipInfo
pure $! isJust $ tableSelectPermissions @b role tableInfo'

View File

@ -318,7 +318,7 @@ validateCustomTypeDefinitions sources customTypes allScalars = do
_trdType
_siName
_siConfiguration
remoteTableInfo
(tableInfoName remoteTableInfo)
annotatedFieldMapping
pure $

View File

@ -178,6 +178,8 @@ annColExp rhsParser rootFieldInfoMap colInfoMap (ColExp fieldName colVal) = do
colInfo <- askFieldInfo colInfoMap fieldName
case colInfo of
FIColumn pgi -> AVColumn pgi <$> parseBoolExpOperations (_berpValueParser rhsParser) rootFieldInfoMap colInfoMap (ColumnReferenceColumn pgi) colVal
FINestedObject {} ->
throw400 NotSupported "nested object not supported"
FIRelationship relInfo -> do
relBoolExp <- decodeValue colVal
relFieldInfoMap <- askFieldInfoMapSource $ riRTable relInfo

View File

@ -666,7 +666,8 @@ buildSchemaCacheRule logger env = proc (MetadataWithResourceVersion metadataNoDe
interpretWriter
-< for (tablesRawInfo `alignTableMap` nonColumnsByTable) \(tableRawInfo, nonColumnInput) -> do
let columns = _tciFieldInfoMap tableRawInfo
allFields :: FieldInfoMap (FieldInfo b) <- addNonColumnFields allSources sourceName tablesRawInfo columns remoteSchemaMap dbFunctions nonColumnInput
customObjectTypes = _tciCustomObjectTypes tableRawInfo
allFields :: FieldInfoMap (FieldInfo b) <- addNonColumnFields allSources sourceName customObjectTypes tablesRawInfo columns remoteSchemaMap dbFunctions nonColumnInput
pure $ tableRawInfo {_tciFieldInfoMap = allFields}
-- permissions

View File

@ -38,25 +38,26 @@ addNonColumnFields ::
) =>
HashMap SourceName (AB.AnyBackend PartiallyResolvedSource) ->
SourceName ->
HashMap G.Name (TableObjectType b) ->
HashMap (TableName b) (TableCoreInfoG b (ColumnInfo b) (ColumnInfo b)) ->
FieldInfoMap (ColumnInfo b) ->
PartiallyResolvedRemoteSchemaMap ->
DBFunctionsMetadata b ->
NonColumnTableInputs b ->
m (FieldInfoMap (FieldInfo b))
addNonColumnFields allSources source rawTableInfo columns remoteSchemaMap pgFunctions NonColumnTableInputs {..} = do
addNonColumnFields allSources source customObjectTypes rawTableInfos columns remoteSchemaMap pgFunctions NonColumnTableInputs {..} = do
objectRelationshipInfos <-
buildInfoMapPreservingMetadataM
_rdName
(mkRelationshipMetadataObject @b ObjRel source _nctiTable)
(buildObjectRelationship (_tciForeignKeys <$> rawTableInfo) source _nctiTable)
(buildObjectRelationship (_tciForeignKeys <$> rawTableInfos) source _nctiTable)
_nctiObjectRelationships
arrayRelationshipInfos <-
buildInfoMapPreservingMetadataM
_rdName
(mkRelationshipMetadataObject @b ArrRel source _nctiTable)
(buildArrayRelationship (_tciForeignKeys <$> rawTableInfo) source _nctiTable)
(buildArrayRelationship (_tciForeignKeys <$> rawTableInfos) source _nctiTable)
_nctiArrayRelationships
let relationshipInfos = objectRelationshipInfos <> arrayRelationshipInfos
@ -65,7 +66,7 @@ addNonColumnFields allSources source rawTableInfo columns remoteSchemaMap pgFunc
buildInfoMapPreservingMetadataM
_cfmName
(mkComputedFieldMetadataObject source _nctiTable)
(buildComputedField (HS.fromList $ M.keys rawTableInfo) (HS.fromList $ map ciColumn $ M.elems columns) source pgFunctions _nctiTable)
(buildComputedField (HS.fromList $ M.keys rawTableInfos) (HS.fromList $ map ciColumn $ M.elems columns) source pgFunctions _nctiTable)
_nctiComputedFields
-- the fields that can be used for defining join conditions to other sources/remote schemas:
-- 1. all columns
@ -151,7 +152,7 @@ addNonColumnFields allSources source rawTableInfo columns remoteSchemaMap pgFunc
return (fieldInfo, metadata)
noColumnConflicts = \case
This columnInfo -> pure $ FIColumn columnInfo
This columnInfo -> pure $ columnInfoToFieldInfo customObjectTypes columnInfo
That (fieldInfo, _) -> pure $ fieldInfo
These columnInfo (_, fieldMetadata) -> do
recordInconsistencyM Nothing fieldMetadata "field definition conflicts with postgres column"

View File

@ -482,6 +482,7 @@ updateColExp qt rf (ColExp fld val) =
Nothing -> pure val
Just fi -> case fi of
FIColumn _ -> pure val
FINestedObject _ -> pure val
FIComputedField _ -> pure val
FIRelationship ri -> do
let remTable = riRTable ri

View File

@ -518,7 +518,8 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
_tciCustomConfig = config,
_tciDescription = description,
_tciExtraTableMetadata = _ptmiExtraTableMetadata metadataTable,
_tciApolloFederationConfig = apolloFedConfig
_tciApolloFederationConfig = apolloFedConfig,
_tciCustomObjectTypes = fromMaybe mempty $ _ptmiCustomObjectTypes metadataTable
}
-- Step 2: Process the raw table cache to replace Postgres column types with logical column

View File

@ -36,6 +36,8 @@ module Hasura.RQL.IR.Select
AnnFieldG (..),
AnnFields,
AnnFieldsG,
AnnNestedObjectSelectG (..),
AnnNestedObjectSelect,
AnnObjectSelect,
AnnObjectSelectG (..),
AnnRelationSelectG (..),
@ -96,6 +98,9 @@ module Hasura.RQL.IR.Select
aarRelationshipName,
aarColumnMapping,
aarAnnSelect,
anosSupportsNestedObjects,
anosColumn,
anosFields,
aosFields,
aosTableFrom,
aosTableFilter,
@ -648,6 +653,9 @@ data AnnFieldG (b :: BackendType) (r :: Type) v
AFRemote (RemoteRelationshipSelect b r)
| AFNodeId (XRelay b) SourceName (TableName b) (PrimaryKeyColumns b)
| AFExpression Text
| -- | Nested object.
AFNestedObject (AnnNestedObjectSelectG b r v)
-- TODO (dmoverton): add AFNestedArray
deriving stock (Functor, Foldable, Traversable)
deriving stock instance
@ -656,7 +664,8 @@ deriving stock instance
Eq (ArraySelectG b r v),
Eq (ComputedFieldSelect b r v),
Eq (ObjectRelationSelectG b r v),
Eq (RemoteRelationshipSelect b r)
Eq (RemoteRelationshipSelect b r),
Eq (AnnNestedObjectSelectG b r v)
) =>
Eq (AnnFieldG b r v)
@ -666,7 +675,8 @@ deriving stock instance
Show (ArraySelectG b r v),
Show (ComputedFieldSelect b r v),
Show (ObjectRelationSelectG b r v),
Show (RemoteRelationshipSelect b r)
Show (RemoteRelationshipSelect b r),
Show (AnnNestedObjectSelectG b r v)
) =>
Show (AnnFieldG b r v)
@ -679,6 +689,7 @@ instance Backend b => Bifoldable (AnnFieldG b) where
AFRemote r -> foldMap f r
AFNodeId {} -> mempty
AFExpression {} -> mempty
AFNestedObject no -> bifoldMap f g no
type AnnField b = AnnFieldG b Void (SQLExpression b)
@ -1059,6 +1070,33 @@ deriving stock instance
) =>
Show (RemoteSourceSelect r vf tgt)
-- Nested objects
data AnnNestedObjectSelectG (b :: BackendType) (r :: Type) v = AnnNestedObjectSelectG
{ _anosSupportsNestedObjects :: XNestedObjects b,
_anosColumn :: Column b,
_anosFields :: AnnFieldsG b r v
}
deriving stock (Functor, Foldable, Traversable)
deriving stock instance
( Backend b,
Eq (AnnFieldsG b r v)
) =>
Eq (AnnNestedObjectSelectG b r v)
deriving stock instance
( Backend b,
Show (AnnFieldsG b r v)
) =>
Show (AnnNestedObjectSelectG b r v)
instance Backend b => Bifoldable (AnnNestedObjectSelectG b) where
bifoldMap f g AnnNestedObjectSelectG {..} =
foldMap (foldMap $ bifoldMap f g) _anosFields
type AnnNestedObjectSelect b r = AnnNestedObjectSelectG b r (SQLExpression b)
-- Permissions
data TablePermG (b :: BackendType) v = TablePerm
@ -1121,6 +1159,7 @@ data CountDistinct
$(makeLenses ''AnnSelectG)
$(makeLenses ''AnnObjectSelectG)
$(makeLenses ''AnnNestedObjectSelectG)
$(makeLenses ''AnnRelationSelectG)
$(makeLenses ''ConnectionSelect)
$(makeLenses ''SelectArgsG)

View File

@ -161,6 +161,13 @@ class
Show (XRelay b),
Eq (XStreamingSubscription b),
Show (XStreamingSubscription b),
Eq (XNestedObjects b),
Ord (XNestedObjects b),
Show (XNestedObjects b),
NFData (XNestedObjects b),
Hashable (XNestedObjects b),
ToJSON (XNestedObjects b),
ToTxt (XNestedObjects b),
-- Intermediate Representations
Traversable (BooleanOperators b),
Traversable (UpdateVariant b),
@ -302,6 +309,9 @@ class
type XStreamingSubscription b :: Type
type XNestedObjects b :: Type
type XNestedObjects b = XDisable
-- The result of dynamic connection template resolution
type ResolvedConnectionTemplate b :: Type
type ResolvedConnectionTemplate b = () -- Uninmplemented value

View File

@ -16,6 +16,7 @@ module Hasura.RQL.Types.Column
ColumnValue (..),
ColumnMutability (..),
ColumnInfo (..),
NestedObjectInfo (..),
RawColumnInfo (..),
PrimaryKeyColumns,
getColInfos,
@ -249,6 +250,31 @@ instance Backend b => ToJSON (ColumnInfo b) where
toJSON = genericToJSON hasuraJSON
toEncoding = genericToEncoding hasuraJSON
data NestedObjectInfo b = NestedObjectInfo
{ _noiSupportsNestedObjects :: XNestedObjects b,
_noiColumn :: Column b,
_noiName :: G.Name,
_noiType :: G.Name,
_noiIsNullable :: Bool,
_noiDescription :: Maybe G.Description,
_noiMutability :: ColumnMutability
}
deriving (Generic)
deriving instance (Backend b) => Eq (NestedObjectInfo b)
deriving instance (Backend b) => Ord (NestedObjectInfo b)
deriving instance (Backend b) => Show (NestedObjectInfo b)
instance (Backend b) => NFData (NestedObjectInfo b)
instance (Backend b) => Hashable (NestedObjectInfo b)
instance (Backend b) => ToJSON (NestedObjectInfo b) where
toJSON = genericToJSON hasuraJSON
toEncoding = genericToEncoding hasuraJSON
type PrimaryKeyColumns b = NESeq (ColumnInfo b)
onlyNumCols :: forall b. Backend b => [ColumnInfo b] -> [ColumnInfo b]

View File

@ -45,7 +45,7 @@ module Hasura.RQL.Types.CustomTypes
)
where
import Autodocodec (HasCodec (codec), bimapCodec, dimapCodec, optionalField', optionalFieldWith', optionalFieldWithDefault', optionalFieldWithOmittedDefault', requiredField', requiredFieldWith')
import Autodocodec (HasCodec (codec), dimapCodec, optionalField', optionalFieldWith', optionalFieldWithDefault', optionalFieldWithOmittedDefault', requiredField', requiredFieldWith')
import Autodocodec qualified as AC
import Autodocodec.Extended (graphQLEnumValueCodec, graphQLFieldDescriptionCodec, graphQLFieldNameCodec, typeableName)
import Control.Lens.TH (makeLenses)
@ -54,7 +54,6 @@ import Data.Aeson qualified as J
import Data.Aeson.TH qualified as J
import Data.HashMap.Strict qualified as Map
import Data.HashSet qualified as Set
import Data.Text qualified as T
import Data.Text.Extended (ToTxt (..))
import Data.Typeable (Typeable)
import Hasura.Backends.Postgres.Instances.Types ()
@ -67,47 +66,11 @@ import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Table
import Hasura.SQL.AnyBackend
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Parser qualified as GParse
import Language.GraphQL.Draft.Printer qualified as GPrint
import Language.GraphQL.Draft.Syntax qualified as G
import Text.Builder qualified as T
--------------------------------------------------------------------------------
-- Metadata
-- | A wrapper around 'G.GType' which allows us to define custom JSON
-- instances.
--
-- TODO: this name is ambiguous, and conflicts with
-- Hasura.RQL.DDL.RemoteSchema.Permission.GraphQLType; it should perhaps be
-- renamed, made internal to this module, or removed altogether?
newtype GraphQLType = GraphQLType {unGraphQLType :: G.GType}
deriving (Show, Eq, Ord, Generic, NFData)
instance HasCodec GraphQLType where
codec = bimapCodec dec enc codec
where
dec t = case GParse.parseGraphQLType t of
Left _ -> Left $ "not a valid GraphQL type: " <> T.unpack t
Right a -> Right $ GraphQLType a
enc = T.run . GPrint.graphQLType . unGraphQLType
instance J.ToJSON GraphQLType where
toJSON = J.toJSON . T.run . GPrint.graphQLType . unGraphQLType
instance J.FromJSON GraphQLType where
parseJSON =
J.withText "GraphQLType" $ \t ->
case GParse.parseGraphQLType t of
Left _ -> fail $ "not a valid GraphQL type: " <> T.unpack t
Right a -> return $ GraphQLType a
isListType :: GraphQLType -> Bool
isListType = coerce G.isListType
isNullableType :: GraphQLType -> Bool
isNullableType = coerce G.isNullable
isInBuiltScalar :: Text -> Bool
isInBuiltScalar s
| s == G.unName GName._Int = True
@ -418,7 +381,7 @@ data AnnotatedTypeRelationship = AnnotatedTypeRelationship
_atrSource :: SourceName,
_atrSourceConfig :: SourceConfig ('Postgres 'Vanilla),
-- TODO: see comment in 'TypeRelationship'
_atrTableInfo :: TableInfo ('Postgres 'Vanilla),
_atrTableName :: TableName ('Postgres 'Vanilla),
_atrFieldMapping :: HashMap ObjectFieldName (ColumnInfo ('Postgres 'Vanilla))
}
deriving (Generic)

View File

@ -34,6 +34,7 @@ import Hasura.SQL.Backend
import Hasura.SQL.Types
import Hasura.Server.Migrate.Version
import Hasura.Services.Network
import Language.GraphQL.Draft.Syntax qualified as G
import Network.HTTP.Client qualified as HTTP
class
@ -208,6 +209,15 @@ class
validateLogicalModel _ _ _ _ =
throw500 "validateLogicalModel: not implemented for this backend."
-- | How to convert a column to a field.
-- For backends that don't support nested objects or arrays the default implementation
-- (i.e. wrapping the ColumnInfo in FIColumn) is what you want.
columnInfoToFieldInfo ::
HashMap G.Name (TableObjectType b) ->
ColumnInfo b ->
FieldInfo b
columnInfoToFieldInfo _ = FIColumn
-- | Allows the backend to control whether or not a particular source supports being
-- the target of remote relationships or not
supportsBeingRemoteRelationshipTarget :: SourceConfig b -> Bool

View File

@ -13,6 +13,7 @@ module Hasura.RQL.Types.Table
FieldInfoMap,
ForeignKey (..),
ForeignKeyMetadata (..),
GraphQLType (..),
InsPermInfo (..),
PrimaryKey (..),
RolePermInfo (..),
@ -26,6 +27,9 @@ module Hasura.RQL.Types.Table
TableCoreInfoG (..),
TableCustomRootFields (..),
TableInfo (..),
TableObjectType (..),
TableObjectFieldDefinition (..),
TableObjectFieldType (..),
UniqueConstraint (..),
UpdPermInfo (..),
ViewInfo (..),
@ -49,7 +53,9 @@ module Hasura.RQL.Types.Table
getFieldInfoM,
getRels,
getRemoteFieldInfoName,
isListType,
isMutable,
isNullableType,
mkAdminRolePermInfo,
permDel,
permIns,
@ -68,6 +74,7 @@ module Hasura.RQL.Types.Table
tciCustomConfig,
tciDescription,
tciApolloFederationConfig,
tciCustomObjectTypes,
tciEnumValues,
tciExtraTableMetadata,
tciFieldInfoMap,
@ -104,6 +111,7 @@ import Autodocodec
import Autodocodec qualified as AC
import Autodocodec.Extended (graphQLFieldNameCodec)
import Control.Lens hiding ((.=))
import Data.Aeson qualified as J
import Data.Aeson.Casing
import Data.Aeson.Extended
import Data.Aeson.TH
@ -136,7 +144,43 @@ import Hasura.SQL.AnyBackend (runBackend)
import Hasura.SQL.Backend
import Hasura.Server.Utils (englishList)
import Hasura.Session
import Language.GraphQL.Draft.Parser qualified as GParse
import Language.GraphQL.Draft.Printer qualified as GPrint
import Language.GraphQL.Draft.Syntax qualified as G
import Text.Builder qualified as T
-- | A wrapper around 'G.GType' which allows us to define custom JSON
-- instances.
--
-- TODO: this name is ambiguous, and conflicts with
-- Hasura.RQL.DDL.RemoteSchema.Permission.GraphQLType; it should perhaps be
-- renamed, made internal to this module, or removed altogether?
newtype GraphQLType = GraphQLType {unGraphQLType :: G.GType}
deriving (Show, Eq, Ord, Generic, NFData)
instance HasCodec GraphQLType where
codec = AC.bimapCodec dec enc codec
where
dec t = case GParse.parseGraphQLType t of
Left _ -> Left $ "not a valid GraphQL type: " <> T.unpack t
Right a -> Right $ GraphQLType a
enc = T.run . GPrint.graphQLType . unGraphQLType
instance J.ToJSON GraphQLType where
toJSON = J.toJSON . T.run . GPrint.graphQLType . unGraphQLType
instance J.FromJSON GraphQLType where
parseJSON =
J.withText "GraphQLType" $ \t ->
case GParse.parseGraphQLType t of
Left _ -> fail $ "not a valid GraphQL type: " <> T.unpack t
Right a -> return $ GraphQLType a
isListType :: GraphQLType -> Bool
isListType = coerce G.isListType
isNullableType :: GraphQLType -> Bool
isNullableType = coerce G.isNullable
data CustomRootField = CustomRootField
{ _crfName :: Maybe G.Name,
@ -300,6 +344,7 @@ getAllCustomRootFields TableCustomRootFields {..} =
data FieldInfo (b :: BackendType)
= FIColumn (ColumnInfo b)
| FINestedObject (NestedObjectInfo b)
| FIRelationship (RelInfo b)
| FIComputedField (ComputedFieldInfo b)
| FIRemoteRelationship (RemoteFieldInfo (DBJoinField b))
@ -322,6 +367,7 @@ type FieldInfoMap = M.HashMap FieldName
fieldInfoName :: forall b. Backend b => FieldInfo b -> FieldName
fieldInfoName = \case
FIColumn info -> fromCol @b $ ciColumn info
FINestedObject info -> fromCol @b $ _noiColumn info
FIRelationship info -> fromRel $ riName info
FIComputedField info -> fromComputedField $ _cfiName info
FIRemoteRelationship info -> fromRemoteRelationship $ getRemoteFieldInfoName info
@ -329,6 +375,7 @@ fieldInfoName = \case
fieldInfoGraphQLName :: FieldInfo b -> Maybe G.Name
fieldInfoGraphQLName = \case
FIColumn info -> Just $ ciName info
FINestedObject info -> Just $ _noiName info
FIRelationship info -> G.mkName $ relNameToTxt $ riName info
FIComputedField info -> G.mkName $ computedFieldNameToText $ _cfiName info
FIRemoteRelationship info -> G.mkName $ relNameToTxt $ getRemoteFieldInfoName info
@ -344,6 +391,7 @@ getRemoteFieldInfoName RemoteFieldInfo {_rfiRHS} = case _rfiRHS of
fieldInfoGraphQLNames :: FieldInfo b -> [G.Name]
fieldInfoGraphQLNames info = case info of
FIColumn _ -> maybeToList $ fieldInfoGraphQLName info
FINestedObject _ -> maybeToList $ fieldInfoGraphQLName info
FIRelationship relationshipInfo -> fold do
name <- fieldInfoGraphQLName info
pure $ case riType relationshipInfo of
@ -913,6 +961,63 @@ instance Backend b => ToJSON (ForeignKey b) where
instance Backend b => FromJSON (ForeignKey b) where
parseJSON = genericParseJSON hasuraJSON
data TableObjectType (b :: BackendType) = TableObjectType
{ _totName :: G.Name,
_totDescription :: Maybe G.Description,
_totFields :: NonEmpty (TableObjectFieldDefinition b)
}
deriving stock (Generic)
deriving stock instance Backend b => Eq (TableObjectType b)
deriving stock instance Backend b => Show (TableObjectType b)
instance Backend b => NFData (TableObjectType b)
instance Backend b => ToJSON (TableObjectType b) where
toJSON = genericToJSON hasuraJSON
instance Backend b => FromJSON (TableObjectType b) where
parseJSON = genericParseJSON hasuraJSON
data TableObjectFieldDefinition (b :: BackendType) = TableObjectFieldDefinition
{ _tofdColumn :: Column b,
_tofdName :: G.Name,
_tofdDescription :: Maybe G.Description,
_tofdGType :: GraphQLType,
_tofdFieldType :: TableObjectFieldType b
}
deriving stock (Generic)
deriving stock instance Backend b => Eq (TableObjectFieldDefinition b)
deriving stock instance Backend b => Show (TableObjectFieldDefinition b)
instance Backend b => NFData (TableObjectFieldDefinition b)
instance Backend b => ToJSON (TableObjectFieldDefinition b) where
toJSON = genericToJSON hasuraJSON
instance Backend b => FromJSON (TableObjectFieldDefinition b) where
parseJSON = genericParseJSON hasuraJSON
data TableObjectFieldType (b :: BackendType)
= TOFTScalar G.Name (ScalarType b)
| TOFTObject G.Name
deriving stock (Generic)
deriving stock instance Backend b => Eq (TableObjectFieldType b)
deriving stock instance Backend b => Show (TableObjectFieldType b)
instance Backend b => NFData (TableObjectFieldType b)
instance Backend b => ToJSON (TableObjectFieldType b) where
toJSON = genericToJSON hasuraJSON
instance Backend b => FromJSON (TableObjectFieldType b) where
parseJSON = genericParseJSON hasuraJSON
-- | The @field@ and @primaryKeyColumn@ type parameters vary as the schema cache is built and more
-- information is accumulated. See also 'TableCoreInfo'.
data TableCoreInfoG (b :: BackendType) field primaryKeyColumn = TableCoreInfo
@ -927,7 +1032,8 @@ data TableCoreInfoG (b :: BackendType) field primaryKeyColumn = TableCoreInfo
_tciEnumValues :: Maybe EnumValues,
_tciCustomConfig :: TableConfig b,
_tciExtraTableMetadata :: ExtraTableMetadata b,
_tciApolloFederationConfig :: Maybe ApolloFederationConfig
_tciApolloFederationConfig :: Maybe ApolloFederationConfig,
_tciCustomObjectTypes :: HashMap G.Name (TableObjectType b)
}
deriving (Generic)
@ -1044,7 +1150,8 @@ data DBTableMetadata (b :: BackendType) = DBTableMetadata
_ptmiForeignKeys :: HashSet (ForeignKeyMetadata b),
_ptmiViewInfo :: Maybe ViewInfo,
_ptmiDescription :: Maybe Postgres.PGDescription,
_ptmiExtraTableMetadata :: ExtraTableMetadata b
_ptmiExtraTableMetadata :: ExtraTableMetadata b,
_ptmiCustomObjectTypes :: Maybe (HashMap G.Name (TableObjectType b))
}
deriving (Generic)
@ -1099,6 +1206,7 @@ askColInfo m c msg = do
askFieldInfo m (fromCol @backend c)
case fieldInfo of
(FIColumn colInfo) -> pure colInfo
(FINestedObject _) -> throwErr "nested object"
(FIRelationship _) -> throwErr "relationship"
(FIComputedField _) -> throwErr "computed field"
(FIRemoteRelationship _) -> throwErr "remote relationship"
@ -1124,6 +1232,7 @@ askComputedFieldInfo fields computedField = do
fromComputedField computedField
case fieldInfo of
(FIColumn _) -> throwErr "column"
(FINestedObject _) -> throwErr "nested object"
(FIRelationship _) -> throwErr "relationship"
(FIRemoteRelationship _) -> throwErr "remote relationship"
(FIComputedField cci) -> pure cci

View File

@ -16,9 +16,11 @@ import Test.Hspec
spec :: Spec
spec = do
describe "SchemaResponse" $ do
testToFromJSONToSchema (SchemaResponse []) [aesonQQ|{"tables": []}|]
testToFromJSONToSchema (SchemaResponse [] Nothing) [aesonQQ|{"tables": []}|]
jsonOpenApiProperties genSchemaResponse
genSchemaResponse :: (MonadGen m, GenBase m ~ Identity) => m SchemaResponse
genSchemaResponse =
SchemaResponse <$> Gen.list defaultRange genTableInfo
SchemaResponse <$> Gen.list defaultRange genTableInfo <*> pure Nothing
-- TODO: fix generator to add GraphQLTypes

View File

@ -144,7 +144,8 @@ buildTableInfo TableInfoBuilder {..} = tableInfo
_tciEnumValues = Nothing,
_tciCustomConfig = tableConfig,
_tciExtraTableMetadata = (),
_tciApolloFederationConfig = Nothing
_tciApolloFederationConfig = Nothing,
_tciCustomObjectTypes = mempty
}
pk :: Maybe (PrimaryKey PG (ColumnInfo PG))