diff --git a/dc-agents/dc-api-types/package.json b/dc-agents/dc-api-types/package.json index 73925f78c63..eb0555cc94e 100644 --- a/dc-agents/dc-api-types/package.json +++ b/dc-agents/dc-api-types/package.json @@ -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", diff --git a/dc-agents/dc-api-types/src/agent.openapi.json b/dc-agents/dc-api-types/src/agent.openapi.json index 1a0c43f4a2f..c8689687aea 100644 --- a/dc-agents/dc-api-types/src/agent.openapi.json +++ b/dc-agents/dc-api-types/src/agent.openapi.json @@ -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" }, diff --git a/dc-agents/dc-api-types/src/index.ts b/dc-agents/dc-api-types/src/index.ts index 8e37911d036..64e7730c8e4 100644 --- a/dc-agents/dc-api-types/src/index.ts +++ b/dc-agents/dc-api-types/src/index.ts @@ -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'; diff --git a/dc-agents/dc-api-types/src/models/Field.ts b/dc-agents/dc-api-types/src/models/Field.ts index 8658122d108..097deeeba11 100644 --- a/dc-agents/dc-api-types/src/models/Field.ts +++ b/dc-agents/dc-api-types/src/models/Field.ts @@ -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); diff --git a/dc-agents/dc-api-types/src/models/NestedObjectField.ts b/dc-agents/dc-api-types/src/models/NestedObjectField.ts new file mode 100644 index 00000000000..a03314f8d19 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/NestedObjectField.ts @@ -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'; +}; + diff --git a/dc-agents/dc-api-types/src/models/ObjectTypeDefinition.ts b/dc-agents/dc-api-types/src/models/ObjectTypeDefinition.ts new file mode 100644 index 00000000000..f61f5429f1f --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ObjectTypeDefinition.ts @@ -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; + /** + * The description of the type + */ + description?: string; + /** + * The name of the type + */ + name: string; +}; + diff --git a/dc-agents/dc-api-types/src/models/SchemaResponse.ts b/dc-agents/dc-api-types/src/models/SchemaResponse.ts index ab8d6b907b2..cb4e9136ec9 100644 --- a/dc-agents/dc-api-types/src/models/SchemaResponse.ts +++ b/dc-agents/dc-api-types/src/models/SchemaResponse.ts @@ -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; /** * Available tables */ diff --git a/dc-agents/package-lock.json b/dc-agents/package-lock.json index 86024f7ef82..2307ff31861 100644 --- a/dc-agents/package-lock.json +++ b/dc-agents/package-lock.json @@ -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", diff --git a/dc-agents/reference/package-lock.json b/dc-agents/reference/package-lock.json index 2aa34dd1deb..7d8d82fa3da 100644 --- a/dc-agents/reference/package-lock.json +++ b/dc-agents/reference/package-lock.json @@ -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", diff --git a/dc-agents/reference/package.json b/dc-agents/reference/package.json index d5ad82e71aa..5293b68aa22 100644 --- a/dc-agents/reference/package.json +++ b/dc-agents/reference/package.json @@ -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", diff --git a/dc-agents/reference/src/query.ts b/dc-agents/reference/src/query.ts index 9f25502d3a1..14a0071d371 100644 --- a/dc-agents/reference/src/query.ts +++ b/dc-agents/reference/src/query.ts @@ -452,6 +452,9 @@ const projectRow = (fields: Record, 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"]); } diff --git a/dc-agents/sqlite/package-lock.json b/dc-agents/sqlite/package-lock.json index 5e2c0878c7d..c9910654f0c 100644 --- a/dc-agents/sqlite/package-lock.json +++ b/dc-agents/sqlite/package-lock.json @@ -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", diff --git a/dc-agents/sqlite/package.json b/dc-agents/sqlite/package.json index df2b2f9355a..22d6917a718 100644 --- a/dc-agents/sqlite/package.json +++ b/dc-agents/sqlite/package.json @@ -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", diff --git a/dc-agents/sqlite/src/query.ts b/dc-agents/sqlite/src/query.ts index 1ca17c33129..68b1b2f17af 100644 --- a/dc-agents/sqlite/src/query.ts +++ b/dc-agents/sqlite/src/query.ts @@ -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"]); } diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs index 122f39ec8bf..2144124fde6 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs @@ -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) diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Schema.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Schema.hs index 9823c4bcf9d..0090556951f 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Schema.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Schema.hs @@ -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 diff --git a/server/lib/dc-api/test/Test/Data.hs b/server/lib/dc-api/test/Test/Data.hs index cb0e8c160c1..70d8870d0c0 100644 --- a/server/lib/dc-api/test/Test/Data.hs +++ b/server/lib/dc-api/test/Test/Data.hs @@ -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) diff --git a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs index 65b69caea0f..2f5c581cc36 100644 --- a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs +++ b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs @@ -826,7 +826,8 @@ schema = API._tiUpdatable = True, API._tiDeletable = True } - ] + ], + _srObjectTypes = Nothing } -- | Stock 'MockConfig' for a Chinook Agent. diff --git a/server/src-lib/Hasura/Backends/BigQuery/DDL/Source.hs b/server/src-lib/Hasura/Backends/BigQuery/DDL/Source.hs index fbe07f41644..0c98726d8c1 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/DDL/Source.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/DDL/Source.hs @@ -132,7 +132,8 @@ resolveSource sourceConfig = _ptmiForeignKeys = mempty, _ptmiViewInfo = Just $ ViewInfo False False False, _ptmiDescription = Nothing, - _ptmiExtraTableMetadata = () + _ptmiExtraTableMetadata = (), + _ptmiCustomObjectTypes = mempty } ) | (index, RestTable {tableReference, schema}) <- diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs index 6f1d75aa7a6..4ab8c36f1fe 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs index 8b5aeaa5074..05db9065e5b 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs index 75256ab176e..06db66318f9 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs @@ -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 => diff --git a/server/src-lib/Hasura/Backends/MSSQL/Meta.hs b/server/src-lib/Hasura/Backends/MSSQL/Meta.hs index 32ed32e1e75..cb5ed8bc673 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Meta.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Meta.hs @@ -155,6 +155,7 @@ transformTable tableInfo = Nothing -- no views, only tables Nothing -- no description identityColumns + mempty ) transformColumn :: diff --git a/server/src-lib/Hasura/Backends/MySQL/Meta.hs b/server/src-lib/Hasura/Backends/MySQL/Meta.hs index 5be0c749f89..0af6b88f741 100644 --- a/server/src-lib/Hasura/Backends/MySQL/Meta.hs +++ b/server/src-lib/Hasura/Backends/MySQL/Meta.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Collect.hs b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Collect.hs index 636bc5fe3d1..1d7b4075b76 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Collect.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Collect.hs @@ -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 = diff --git a/server/src-lib/Hasura/GraphQL/Schema/Action.hs b/server/src-lib/Hasura/GraphQL/Schema/Action.hs index badc2609fef..c283dccda31 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Action.hs @@ -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 } } diff --git a/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs index 061fcc1733f..e05d4febde0 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs @@ -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 { diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index a0dc4f17976..c1beb09220c 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs b/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs index d5c83c7e163..d70330c86ce 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs index 9c6b4494908..d9dc8ba98e4 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/OrderBy.hs @@ -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. -- diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index 387603b778a..aef024e6710 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -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 $ diff --git a/server/src-lib/Hasura/GraphQL/Schema/Table.hs b/server/src-lib/Hasura/GraphQL/Schema/Table.hs index 0d55f9d039b..4195cce84bc 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Table.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Table.hs @@ -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' diff --git a/server/src-lib/Hasura/RQL/DDL/CustomTypes.hs b/server/src-lib/Hasura/RQL/DDL/CustomTypes.hs index 201b907ed65..e4dd4d28dab 100644 --- a/server/src-lib/Hasura/RQL/DDL/CustomTypes.hs +++ b/server/src-lib/Hasura/RQL/DDL/CustomTypes.hs @@ -318,7 +318,7 @@ validateCustomTypeDefinitions sources customTypes allScalars = do _trdType _siName _siConfiguration - remoteTableInfo + (tableInfoName remoteTableInfo) annotatedFieldMapping pure $ diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index f2de8f1498d..97ecce45c8b 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs index f24ee4533a5..fc87a65ddea 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Fields.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Fields.hs index f145bfe5957..8dbead674dd 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Fields.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Fields.hs @@ -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" diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs index 2720c0d7539..ad68333d360 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs index 96114b66f3f..d0ad3d35ceb 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/IR/Select.hs b/server/src-lib/Hasura/RQL/IR/Select.hs index c1b7baed8dc..766e31c9215 100644 --- a/server/src-lib/Hasura/RQL/IR/Select.hs +++ b/server/src-lib/Hasura/RQL/IR/Select.hs @@ -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) diff --git a/server/src-lib/Hasura/RQL/Types/Backend.hs b/server/src-lib/Hasura/RQL/Types/Backend.hs index 49775c345a8..6886b8a3690 100644 --- a/server/src-lib/Hasura/RQL/Types/Backend.hs +++ b/server/src-lib/Hasura/RQL/Types/Backend.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types/Column.hs b/server/src-lib/Hasura/RQL/Types/Column.hs index 052e4a8ebe0..1fcfcbda704 100644 --- a/server/src-lib/Hasura/RQL/Types/Column.hs +++ b/server/src-lib/Hasura/RQL/Types/Column.hs @@ -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] diff --git a/server/src-lib/Hasura/RQL/Types/CustomTypes.hs b/server/src-lib/Hasura/RQL/Types/CustomTypes.hs index 8925ce42107..d33cfd412b9 100644 --- a/server/src-lib/Hasura/RQL/Types/CustomTypes.hs +++ b/server/src-lib/Hasura/RQL/Types/CustomTypes.hs @@ -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) diff --git a/server/src-lib/Hasura/RQL/Types/Metadata/Backend.hs b/server/src-lib/Hasura/RQL/Types/Metadata/Backend.hs index 18af10f9c20..3f2b4aa2bae 100644 --- a/server/src-lib/Hasura/RQL/Types/Metadata/Backend.hs +++ b/server/src-lib/Hasura/RQL/Types/Metadata/Backend.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types/Table.hs b/server/src-lib/Hasura/RQL/Types/Table.hs index d8ac8d70dfd..f90ff82d187 100644 --- a/server/src-lib/Hasura/RQL/Types/Table.hs +++ b/server/src-lib/Hasura/RQL/Types/Table.hs @@ -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 diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/SchemaSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/SchemaSpec.hs index b5b480d9a1c..a82010bbe98 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/SchemaSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/SchemaSpec.hs @@ -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 diff --git a/server/src-test/Test/Parser/Internal.hs b/server/src-test/Test/Parser/Internal.hs index d82f4f34c22..e2cad1f4ea2 100644 --- a/server/src-test/Test/Parser/Internal.hs +++ b/server/src-test/Test/Parser/Internal.hs @@ -144,7 +144,8 @@ buildTableInfo TableInfoBuilder {..} = tableInfo _tciEnumValues = Nothing, _tciCustomConfig = tableConfig, _tciExtraTableMetadata = (), - _tciApolloFederationConfig = Nothing + _tciApolloFederationConfig = Nothing, + _tciCustomObjectTypes = mempty } pk :: Maybe (PrimaryKey PG (ColumnInfo PG))