Customization of table GraphQL schema descriptions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3570
GitOrigin-RevId: f642a4d9efdd26a344951fcab6c1bbbc1253dfa3
This commit is contained in:
Daniel Chambers 2022-02-10 17:31:44 +11:00 committed by hasura-bot
parent 773870f443
commit dd403f92e2
16 changed files with 230 additions and 16 deletions

View File

@ -21,6 +21,7 @@ The optimization can be enabled using the
(Add entries below in the order of server, console, cli, docs, others)
- console: add support for remote database relationships
- console: enable support for update permissions for mssql #3591
- server: add support for customization of table GraphQL schema descriptions (#7496)
- cli: skip tls verfication for all API requests when `insecure-skip-tls-verify` flag is set (#4926)
- server: classify MSSQL exceptions and improve API error responses

View File

@ -59,7 +59,8 @@ Add a table/view ``author``:
},
"custom_column_names": {
"id": "authorId"
}
},
"comment": "Authors of books"
}
}
}

View File

@ -512,6 +512,12 @@ Table Config
- false
- :ref:`CustomColumnNames`
- Customise the column fields
* - comment
- false
- ``String``
- Customise the description shown in GraphQL introspection. If null or omitted then
if a comment exists on the database table, it is used as the description
(Postgres-only), and if not, an autogenerated description is used instead.
.. _custom_root_fields:

View File

@ -365,10 +365,7 @@ tableSelectionSet sourceName tableInfo selectPermissions = memoizeOn 'tableSelec
let xRelay = relayExtension @b
tableFields = Map.elems $ _tciFieldInfoMap tableCoreInfo
tablePkeyColumns = _pkColumns <$> _tciPrimaryKey tableCoreInfo
description =
Just $
mkDescriptionWith (_tciDescription tableCoreInfo) $
"columns and relationships of " <>> tableName
description = G.Description . PG.getPGDescription <$> _tciDescription tableCoreInfo
fieldParsers <-
concat <$> for tableFields \fieldInfo ->
fieldSelection sourceName tableName tablePkeyColumns fieldInfo selectPermissions

View File

@ -404,11 +404,11 @@ alterCustomColumnNamesInMetadata ::
TableCoreInfo b ->
m ()
alterCustomColumnNamesInMetadata source droppedCols ti = do
let TableConfig customFields customColumnNames customName = _tciCustomConfig ti
tn = _tciName ti
modifiedCustomColumnNames = foldl' (flip M.delete) customColumnNames droppedCols
when (modifiedCustomColumnNames /= customColumnNames) $
let tableConfig@TableConfig {..} = _tciCustomConfig ti
tableName = _tciName ti
modifiedCustomColumnNames = foldl' (flip M.delete) _tcCustomColumnNames droppedCols
when (modifiedCustomColumnNames /= _tcCustomColumnNames) $
tell $
MetadataModifier $
tableMetadataSetter @b source tn . tmConfiguration
.~ TableConfig @b customFields modifiedCustomColumnNames customName
tableMetadataSetter @b source tableName . tmConfiguration
.~ tableConfig {_tcCustomColumnNames = modifiedCustomColumnNames}

View File

@ -31,7 +31,7 @@ import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.HashSet qualified as S
import Data.Text.Extended
import Data.These (These (..))
import Hasura.Backends.Postgres.SQL.Types (QualifiedTable)
import Hasura.Backends.Postgres.SQL.Types (PGDescription (..), QualifiedTable)
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.GraphQL.Context
@ -294,7 +294,7 @@ runSetTableCustomFieldsQV2 ::
(QErrM m, CacheRWM m, MetadataM m) => SetTableCustomFields -> m EncJSON
runSetTableCustomFieldsQV2 (SetTableCustomFields source tableName rootFields columnNames) = do
void $ askTabInfo @('Postgres 'Vanilla) source tableName -- assert that table is tracked
let tableConfig = TableConfig @('Postgres 'Vanilla) rootFields columnNames Nothing
let tableConfig = TableConfig @('Postgres 'Vanilla) rootFields columnNames Nothing Automatic
buildSchemaCacheFor
(MOSourceObjId source $ AB.mkAnyBackend $ SMOTable @('Postgres 'Vanilla) tableName)
$ MetadataModifier $
@ -464,6 +464,7 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
let columns :: [RawColumnInfo b] = _ptmiColumns metadataTable
columnMap = mapFromL (FieldName . toTxt . rciName) columns
primaryKey = _ptmiPrimaryKey metadataTable
description = buildDescription name config metadataTable
rawPrimaryKey <- liftEitherA -< traverse (resolvePrimaryKeyColumns columnMap) primaryKey
enumValues <-
if isEnum
@ -487,7 +488,7 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
_tciViewInfo = _ptmiViewInfo metadataTable,
_tciEnumValues = enumValues,
_tciCustomConfig = config,
_tciDescription = _ptmiDescription metadataTable,
_tciDescription = description,
_tciExtraTableMetadata = _ptmiExtraTableMetadata metadataTable
}
@ -583,3 +584,12 @@ buildTableCache = Inc.cache proc (source, sourceConfig, dbTablesMeta, tableBuild
<> englishList "and" (dquote . ciColumn <$> (one :| two : more))
<> " are in conflict: they are mapped to the same field name, " <>> name
_ -> pure ()
buildDescription :: TableName b -> TableConfig b -> DBTableMetadata b -> Maybe PGDescription
buildDescription tableName tableConfig tableMetadata =
case _tcComment tableConfig of
Automatic -> _ptmiDescription tableMetadata <|> Just autogeneratedDescription
Explicit description -> PGDescription . toTxt <$> description
where
autogeneratedDescription =
PGDescription $ "columns and relationships of " <>> tableName

View File

@ -7,6 +7,7 @@ module Hasura.RQL.Types.Table
DBTableMetadata (..),
DBTablesMetadata,
DelPermInfo (..),
Comment (..),
FieldInfo (..),
FieldInfoMap,
ForeignKey (..),
@ -55,6 +56,7 @@ module Hasura.RQL.Types.Table
tcCustomColumnNames,
tcCustomName,
tcCustomRootFields,
tcComment,
tciCustomConfig,
tciDescription,
tciEnumValues,
@ -83,6 +85,7 @@ import Control.Lens hiding ((.=))
import Data.Aeson.Casing
import Data.Aeson.Extended
import Data.Aeson.TH
import Data.Aeson.Types (prependFailure, typeMismatch)
import Data.HashMap.Strict qualified as M
import Data.HashMap.Strict.Extended qualified as M
import Data.HashSet qualified as HS
@ -91,6 +94,7 @@ import Data.List.NonEmpty qualified as NE
import Data.Semigroup (Any (..), Max (..))
import Data.Text qualified as T
import Data.Text.Extended
import Data.Text.NonEmpty (NonEmptyText, mkNonEmptyText)
import Hasura.Backends.Postgres.SQL.Types qualified as PG (PGDescription)
import Hasura.Base.Error
import Hasura.Incremental (Cacheable)
@ -577,10 +581,33 @@ isMutable f (Just vi) = f vi
type CustomColumnNames b = HashMap (Column b) G.Name
data Comment
= -- | Automatically generate a comment (derive it from DB comments, or a sensible default describing the source of the data)
Automatic
| -- | The user's explicitly provided comment, no explicitly no comment (ie. leave it blank, do not autogenerate one)
Explicit (Maybe NonEmptyText)
deriving (Eq, Show, Generic)
instance NFData Comment
instance Cacheable Comment
instance FromJSON Comment where
parseJSON = \case
Null -> pure Automatic
String text -> pure . Explicit $ mkNonEmptyText text
val -> prependFailure "parsing Comment failed, " (typeMismatch "String or Null" val)
instance ToJSON Comment where
toJSON Automatic = Null
toJSON (Explicit (Just value)) = String (toTxt value)
toJSON (Explicit Nothing) = String ""
data TableConfig b = TableConfig
{ _tcCustomRootFields :: !TableCustomRootFields,
_tcCustomColumnNames :: !(CustomColumnNames b),
_tcCustomName :: !(Maybe G.Name)
_tcCustomName :: !(Maybe G.Name),
_tcComment :: !Comment
}
deriving (Generic)
@ -599,7 +626,7 @@ $(makeLenses ''TableConfig)
emptyTableConfig :: (TableConfig b)
emptyTableConfig =
TableConfig emptyCustomRootFields M.empty Nothing
TableConfig emptyCustomRootFields M.empty Nothing Automatic
instance (Backend b) => FromJSON (TableConfig b) where
parseJSON = withObject "TableConfig" $ \obj ->
@ -607,6 +634,7 @@ instance (Backend b) => FromJSON (TableConfig b) where
<$> obj .:? "custom_root_fields" .!= emptyCustomRootFields
<*> obj .:? "custom_column_names" .!= M.empty
<*> obj .:? "custom_name"
<*> obj .:? "comment" .!= Automatic
data Constraint (b :: BackendType) = Constraint
{ _cName :: !(ConstraintName b),

View File

@ -0,0 +1,18 @@
description: GraphQL introspection query
url: /v1/graphql
status: 200
query:
query: |
query IntrospectionQuery {
__type(name: "automatic_comment_in_db") {
name
description
kind
}
}
response:
data:
__type:
name: automatic_comment_in_db
description: What a great comment in the DB
kind: OBJECT

View File

@ -0,0 +1,18 @@
description: GraphQL introspection query
url: /v1/graphql
status: 200
query:
query: |
query IntrospectionQuery {
__type(name: "automatic_no_comment_in_db") {
name
description
kind
}
}
response:
data:
__type:
name: automatic_no_comment_in_db
description: columns and relationships of "automatic_no_comment_in_db"
kind: OBJECT

View File

@ -0,0 +1,18 @@
description: GraphQL introspection query
url: /v1/graphql
status: 200
query:
query: |
query IntrospectionQuery {
__type(name: "explicit_comment_in_metadata") {
name
description
kind
}
}
response:
data:
__type:
name: explicit_comment_in_metadata
description: Such an explicit comment, wow
kind: OBJECT

View File

@ -0,0 +1,18 @@
description: GraphQL introspection query
url: /v1/graphql
status: 200
query:
query: |
query IntrospectionQuery {
__type(name: "explicit_no_comment_in_metadata") {
name
description
kind
}
}
response:
data:
__type:
name: explicit_no_comment_in_metadata
description: null
kind: OBJECT

View File

@ -0,0 +1,33 @@
type: bulk
args:
- type: run_sql
args:
sql: |
CREATE TABLE "automatic_comment_in_db" (
id serial primary key
);
COMMENT ON TABLE "automatic_comment_in_db" IS 'What a great comment in the DB';
- type: run_sql
args:
sql: |
CREATE TABLE "automatic_no_comment_in_db" (
id serial primary key
);
- type: run_sql
args:
sql: |
CREATE TABLE "explicit_comment_in_metadata" (
id serial primary key
);
COMMENT ON TABLE "explicit_comment_in_metadata" IS 'Fantastic comment, so good, so hidden';
- type: run_sql
args:
sql: |
CREATE TABLE "explicit_no_comment_in_metadata" (
id serial primary key
);
COMMENT ON TABLE "explicit_no_comment_in_metadata" IS 'This would be a great comment, but you can''t see it';

View File

@ -0,0 +1,7 @@
type: run_sql
args:
sql: |
DROP TABLE "automatic_comment_in_db" cascade;
DROP TABLE "automatic_no_comment_in_db" cascade;
DROP TABLE "explicit_comment_in_metadata" cascade;
DROP TABLE "explicit_no_comment_in_metadata" cascade;

View File

@ -0,0 +1,21 @@
type: bulk
args:
- type: pg_track_table
args:
table: automatic_comment_in_db
- type: pg_track_table
args:
table: automatic_no_comment_in_db
- type: pg_track_table
args:
table: explicit_comment_in_metadata
configuration:
comment: Such an explicit comment, wow
- type: pg_track_table
args:
table: explicit_no_comment_in_metadata
configuration:
comment: ""

View File

@ -0,0 +1,17 @@
type: bulk
args:
- type: pg_untrack_table
args:
table: automatic_comment_in_db
- type: pg_untrack_table
args:
table: automatic_no_comment_in_db
- type: pg_untrack_table
args:
table: explicit_comment_in_metadata
- type: pg_untrack_table
args:
table: explicit_no_comment_in_metadata

View File

@ -139,3 +139,24 @@ class TestDisableGraphQLIntrospection:
def test_disable_introspection(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/disable_introspection.yaml")
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestGraphQlIntrospectionDescriptions:
setup_metadata_api_version = "v2"
@classmethod
def dir(cls):
return "queries/graphql_introspection/descriptions"
def test_automatic_comment_in_db(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/automatic_comment_in_db.yaml")
def test_automatic_no_comment_in_db(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/automatic_no_comment_in_db.yaml")
def test_explicit_comment_in_metadata(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/explicit_comment_in_metadata.yaml")
def test_explicit_no_comment_in_metadata(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/explicit_no_comment_in_metadata.yaml")