Customization of computed field GraphQL schema descriptions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3615
GitOrigin-RevId: f51590d4cfc0412be9baa371353f9b9f3b908f84
This commit is contained in:
Daniel Chambers 2022-02-16 10:16:34 +11:00 committed by hasura-bot
parent 6c7820caa0
commit 2c7a4e3a16
23 changed files with 230 additions and 60 deletions

View File

@ -19,6 +19,9 @@ The optimization can be enabled using the
`--experimental-features=optimize_permission_filters` flag or the
`HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` environment variable.
### Breaking changes
* Computed field comments are now used as the description for the field in the GraphQL schema. This means that computed fields where the comment has been set to empty string will cause the description of the field in the GraphQL schema to also be blank. Setting the computed field comment to null will restore the previous auto-generated description. The previous version of the Console would set the comment to empty string if the comment textbox was left blank, so some existing computed fields may unintentionally have empty string set as their comment.
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)
@ -26,7 +29,7 @@ The optimization can be enabled using the
- server: fix nullable action response (issue #4405)
- 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)
- server: add support for customization of table & computed field 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

@ -141,6 +141,10 @@ export const isArrayString = (str: string) => {
return false;
};
export function emptyStringToNull(val?: string): string | null {
return val && val !== '' ? val : null;
}
/* ARRAY utils */
export const deleteArrayElementAtIndex = (array: unknown[], index: number) => {
return array.splice(index, 1);

View File

@ -2,7 +2,10 @@ import React from 'react';
import AceEditor from 'react-ace';
import { OptionTypeBase } from 'react-select';
import { getConfirmation } from '../../../Common/utils/jsUtils';
import {
emptyStringToNull,
getConfirmation,
} from '../../../Common/utils/jsUtils';
import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor';
import RawSqlButton from '../Common/Components/RawSqlButton';
import Tooltip from '../../../Common/Tooltip/Tooltip';
@ -274,7 +277,7 @@ const ComputedFieldsEditor: React.FC<ComputedFieldsEditorProps> = ({
newState[i] = {
...newState[i],
comment: e.target.value,
comment: emptyStringToNull(e.target.value),
};
setComputedFieldsState(newState);

View File

@ -90,8 +90,10 @@ Args syntax
- The computed field definition
* - comment
- false
- text
- comment
- ``String``
- Customise the description shown in GraphQL introspection. If null or omitted then
if a comment exists on the database function, it is used as the description, and
if not, an autogenerated description is used instead.
* - source
- false
- :ref:`SourceName <SourceName>`
@ -156,4 +158,3 @@ Args syntax
- false
- :ref:`SourceName <SourceName>`
- Name of the source database of the table (default: ``default``)

View File

@ -36,7 +36,7 @@ buildComputedFieldInfo ::
ComputedFieldName ->
ComputedFieldDefinition 'BigQuery ->
RawFunctionInfo 'BigQuery ->
Maybe Text ->
Comment ->
m (ComputedFieldInfo 'BigQuery)
buildComputedFieldInfo _ _ _ _ _ _ =
throw400 NotSupported "Computed fields aren't supported for BigQuery sources"

View File

@ -43,7 +43,7 @@ buildComputedFieldInfo ::
ComputedFieldName ->
ComputedFieldDefinition 'MSSQL ->
RawFunctionInfo 'MSSQL ->
Maybe Text ->
Comment ->
m (ComputedFieldInfo 'MSSQL)
buildComputedFieldInfo _ _ _ _ _ _ =
throw400 NotSupported "Computed fields aren't supported for MSSQL sources"

View File

@ -16,6 +16,7 @@ import Hasura.Backends.Postgres.DDL.Function
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types.Common (Comment (..))
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.Function
import Hasura.SQL.Backend
@ -85,7 +86,7 @@ buildComputedFieldInfo ::
ComputedFieldName ->
ComputedFieldDefinition ('Postgres pgKind) ->
PGRawFunctionInfo ->
Maybe Text ->
Comment ->
m (ComputedFieldInfo ('Postgres pgKind))
buildComputedFieldInfo trackedTables table computedField definition rawFunctionInfo comment =
either (throw400 NotSupported . showErrors) pure =<< MV.runValidateT mkComputedFieldInfo
@ -169,7 +170,7 @@ buildComputedFieldInfo trackedTables table computedField definition rawFunctionI
ComputedFieldFunction function inputArgSeq tableArgument maybePGSessionArg $
rfiDescription rawFunctionInfo
pure $ ComputedFieldInfo @('Postgres pgKind) () computedField computedFieldFunction returnType comment
pure $ ComputedFieldInfo @('Postgres pgKind) () computedField computedFieldFunction returnType description
validateTableArgumentType ::
(MV.MonadValidate [ComputedFieldValidateError] n) =>
@ -215,3 +216,13 @@ buildComputedFieldInfo trackedTables table computedField definition rawFunctionI
Just (FunctionSessionArgument name _) ->
filter ((/=) (Just name) . faName) withoutTable
in alsoWithoutSession
description :: Maybe Text
description =
case comment of
Automatic -> commentFromDatabase <|> Just autogeneratedDescription
Explicit value -> toTxt <$> value
where
commentFromDatabase = getPGDescription <$> rfiDescription rawFunctionInfo
autogeneratedDescription =
"A computed field, executes function " <>> function

View File

@ -1356,7 +1356,7 @@ computedFieldPG sourceName ComputedFieldInfo {..} parentTable selectPermissions
caseBoolExpUnpreparedValue
)
dummyParser <- lift $ columnParser @('Postgres pgKind) (ColumnScalar scalarReturnType) (G.Nullability True)
pure $ P.selection fieldName (Just fieldDescription) fieldArgsParser dummyParser
pure $ P.selection fieldName fieldDescription fieldArgsParser dummyParser
CFRSetofTable tableName -> do
tableInfo <- lift $ askTableInfo sourceName tableName
remotePerms <- MaybeT $ tableSelectPermissions tableInfo
@ -1364,7 +1364,7 @@ computedFieldPG sourceName ComputedFieldInfo {..} parentTable selectPermissions
selectionSetParser <- lift $ P.multiple . P.nonNullableParser <$> tableSelectionSet sourceName tableInfo remotePerms
let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser
pure $
P.subselection fieldName (Just fieldDescription) fieldArgsParser selectionSetParser
P.subselection fieldName fieldDescription fieldArgsParser selectionSetParser
<&> \((functionArgs', args), fields) ->
IR.AFComputedField _cfiXComputedFieldInfo _cfiName $
IR.CFSTable JASMultipleRows $
@ -1376,9 +1376,8 @@ computedFieldPG sourceName ComputedFieldInfo {..} parentTable selectPermissions
IR._asnStrfyNum = stringifyNum
}
where
fieldDescription =
let defaultDescription = "A computed field, executes function " <>> _cffName _cfiFunction
in mkDescriptionWith (_cffDescription _cfiFunction) defaultDescription
fieldDescription :: Maybe G.Description
fieldDescription = G.Description <$> _cfiDescription
computedFieldFunctionArgs ::
ComputedFieldFunction ('Postgres pgKind) ->

View File

@ -25,7 +25,7 @@ data AddComputedField b = AddComputedField
_afcTable :: !(TableName b),
_afcName :: !ComputedFieldName,
_afcDefinition :: !(ComputedFieldDefinition b),
_afcComment :: !(Maybe Text)
_afcComment :: !Comment
}
deriving stock (Generic)
@ -39,7 +39,7 @@ instance (Backend b) => FromJSON (AddComputedField b) where
<*> o .: "table"
<*> o .: "name"
<*> o .: "definition"
<*> o .:? "comment"
<*> o .:? "comment" .!= Automatic
runAddComputedField ::
forall b m.

View File

@ -194,12 +194,13 @@ addComputedFieldToCatalog q =
defaultTxErrorHandler
[Q.sql|
INSERT INTO hdb_catalog.hdb_computed_field
(table_schema, table_name, computed_field_name, definition, comment)
(table_schema, table_name, computed_field_name, definition, commentText)
VALUES ($1, $2, $3, $4, $5)
|]
(schemaName, tableName, computedField, Q.AltJ definition, comment)
(schemaName, tableName, computedField, Q.AltJ definition, commentText)
True
where
commentText = commentToMaybeText comment
QualifiedObject schemaName tableName = table
AddComputedField _ table computedField definition comment = q
@ -575,7 +576,7 @@ fetchMetadataFromHdbTables = liftTx do
pure $
flip map r $ \(schema, table, name, Q.AltJ definition, comment) ->
( QualifiedObject schema table,
ComputedFieldMetadata name definition comment
ComputedFieldMetadata name definition (commentFromMaybeText comment)
)
fetchCronTriggers =

View File

@ -43,6 +43,9 @@ module Hasura.RQL.Types.Common
PGConnectionParams (..),
getPGConnectionStringFromParams,
getConnOptionsFromConnParams,
Comment (..),
commentToMaybeText,
commentFromMaybeText,
)
where
@ -50,6 +53,7 @@ import Control.Lens (makeLenses)
import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Aeson.Types (prependFailure, typeMismatch)
import Data.Bifunctor (bimap)
import Data.Environment qualified as Env
import Data.Scientific (toBoundedInteger)
@ -487,3 +491,36 @@ $(deriveJSON (aesonPrefix snakeCase) ''MetricsConfig)
emptyMetricsConfig :: MetricsConfig
emptyMetricsConfig = MetricsConfig False False
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, or 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 Hashable 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 ""
commentToMaybeText :: Comment -> Maybe Text
commentToMaybeText Automatic = Nothing
commentToMaybeText (Explicit Nothing) = Just ""
commentToMaybeText (Explicit (Just val)) = Just (toTxt val)
commentFromMaybeText :: Maybe Text -> Comment
commentFromMaybeText Nothing = Automatic
commentFromMaybeText (Just val) = Explicit $ mkNonEmptyText val

View File

@ -10,7 +10,7 @@ module Hasura.RQL.Types.ComputedField
FunctionSessionArgument (..),
FunctionTableArgument (..),
FunctionTrackedAs (..),
cfiComment,
cfiDescription,
cfiFunction,
cfiName,
cfiReturnType,
@ -28,7 +28,7 @@ import Data.Aeson
import Data.Aeson.Casing
import Data.Sequence qualified as Seq
import Data.Text.Extended
import Data.Text.NonEmpty
import Data.Text.NonEmpty (NonEmptyText (..))
import Database.PG.Query qualified as Q
import Hasura.Backends.Postgres.SQL.Types hiding (FunctionName, TableName)
import Hasura.Incremental (Cacheable)
@ -177,7 +177,7 @@ data ComputedFieldInfo (b :: BackendType) = ComputedFieldInfo
_cfiName :: !ComputedFieldName,
_cfiFunction :: !(ComputedFieldFunction b),
_cfiReturnType :: !(ComputedFieldReturn b),
_cfiComment :: !(Maybe Text)
_cfiDescription :: !(Maybe Text)
}
deriving (Generic)
@ -193,8 +193,8 @@ instance (Backend b) => Hashable (ComputedFieldInfo b)
instance (Backend b) => ToJSON (ComputedFieldInfo b) where
-- spelling out the JSON instance in order to skip the Trees That Grow field
toJSON (ComputedFieldInfo _ name func tp comment) =
object ["name" .= name, "function" .= func, "return_type" .= tp, "comment" .= comment]
toJSON (ComputedFieldInfo _ name func tp description) =
object ["name" .= name, "function" .= func, "return_type" .= tp, "description" .= description]
$(makeLenses ''ComputedFieldInfo)

View File

@ -197,17 +197,26 @@ currentMetadataVersion = MVVersion3
data ComputedFieldMetadata b = ComputedFieldMetadata
{ _cfmName :: !ComputedFieldName,
_cfmDefinition :: !(ComputedFieldDefinition b),
_cfmComment :: !(Maybe Text)
_cfmComment :: !Comment
}
deriving (Show, Eq, Generic)
instance (Backend b) => Cacheable (ComputedFieldMetadata b)
instance (Backend b) => ToJSON (ComputedFieldMetadata b) where
toJSON = genericToJSON hasuraJSON
toJSON ComputedFieldMetadata {..} =
object
[ "name" .= _cfmName,
"definition" .= _cfmDefinition,
"comment" .= _cfmComment
]
instance (Backend b) => FromJSON (ComputedFieldMetadata b) where
parseJSON = genericParseJSON hasuraJSON
parseJSON = withObject "ComputedFieldMetadata" $ \obj ->
ComputedFieldMetadata
<$> obj .: "name"
<*> obj .: "definition"
<*> obj .:? "comment" .!= Automatic
data RemoteSchemaPermissionMetadata = RemoteSchemaPermissionMetadata
{ _rspmRole :: !RoleName,
@ -931,7 +940,7 @@ metadataToOrdJSON
[ ("name", AO.toOrdered name),
("definition", AO.toOrdered definition)
]
<> catMaybes [maybeCommentToMaybeOrdPair comment]
<> catMaybes [commentToMaybeOrdPair comment]
insPermDefToOrdJSON :: forall b. (Backend b) => InsPermDef b -> AO.Value
insPermDefToOrdJSON = permDefToOrdJSON insPermToOrdJSON
@ -1256,6 +1265,9 @@ metadataToOrdJSON
maybeAnyToMaybeOrdPair :: Text -> (a -> AO.Value) -> Maybe a -> Maybe (Text, AO.Value)
maybeAnyToMaybeOrdPair name f = fmap ((name,) . f)
commentToMaybeOrdPair :: Comment -> Maybe (Text, AO.Value)
commentToMaybeOrdPair comment = (\val -> ("comment", AO.toOrdered val)) <$> commentToMaybeText comment
instance ToJSON Metadata where
toJSON = AO.fromOrdered . metadataToOrdJSON

View File

@ -37,7 +37,7 @@ class
ComputedFieldName ->
ComputedFieldDefinition b ->
RawFunctionInfo b ->
Maybe Text ->
Comment ->
m (ComputedFieldInfo b)
fetchAndValidateEnumValues ::

View File

@ -7,7 +7,6 @@ module Hasura.RQL.Types.Table
DBTableMetadata (..),
DBTablesMetadata,
DelPermInfo (..),
Comment (..),
FieldInfo (..),
FieldInfoMap,
ForeignKey (..),
@ -85,7 +84,6 @@ 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
@ -94,7 +92,6 @@ 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)
@ -581,28 +578,6 @@ 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),

View File

@ -1,9 +1,11 @@
module Hasura.RQL.Types.CommonSpec (spec) where
import Data.Text qualified as Text
import Hasura.Prelude
import Hasura.RQL.Types.Common (PGConnectionParams (..), getPGConnectionStringFromParams)
import Hasura.RQL.Types.Common (PGConnectionParams (..), commentFromMaybeText, commentToMaybeText, getPGConnectionStringFromParams)
import Network.URI (isAbsoluteURI)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
noPasswordParams :: PGConnectionParams
noPasswordParams =
@ -35,6 +37,7 @@ escapeCharParams =
spec :: Spec
spec = do
pgConnectionStringFromParamsSpec
commentSpec
pgConnectionStringFromParamsSpec :: Spec
pgConnectionStringFromParamsSpec =
@ -56,3 +59,11 @@ pgConnectionStringFromParamsSpec =
connectionString `shouldBe` "postgresql://r00t:p%40ssw0rd@loc%40lhost:5432/test%2F%2Fdb"
isAbsoluteURI connectionString `shouldBe` True
commentSpec :: Spec
commentSpec =
describe "Comment" $ do
prop "should roundtrip between Comment and Maybe Text" $
\str ->
let text = Text.pack <$> str
in (commentToMaybeText . commentFromMaybeText) text `shouldBe` text

View File

@ -8,6 +8,10 @@ query:
name
description
kind
fields {
name
description
}
}
}
response:
@ -16,3 +20,10 @@ query:
name: automatic_comment_in_db
description: What a great comment in the DB
kind: OBJECT
fields:
- name: id
description: null
- name: name
description: null
- name: upper_name
description: What a great comment on the function in the DB

View File

@ -8,6 +8,10 @@ query:
name
description
kind
fields {
name
description
}
}
}
response:
@ -16,3 +20,10 @@ query:
name: automatic_no_comment_in_db
description: columns and relationships of "automatic_no_comment_in_db"
kind: OBJECT
fields:
- name: id
description: null
- name: name
description: null
- name: upper_name
description: A computed field, executes function automatic_no_comment_in_db_upper_name

View File

@ -8,6 +8,10 @@ query:
name
description
kind
fields {
name
description
}
}
}
response:
@ -16,3 +20,10 @@ query:
name: explicit_comment_in_metadata
description: Such an explicit comment, wow
kind: OBJECT
fields:
- name: id
description: null
- name: name
description: null
- name: upper_name
description: Such an explicit function comment, wow

View File

@ -8,6 +8,10 @@ query:
name
description
kind
fields {
name
description
}
}
}
response:
@ -16,3 +20,10 @@ query:
name: explicit_no_comment_in_metadata
description: null
kind: OBJECT
fields:
- name: id
description: null
- name: name
description: null
- name: upper_name
description: null

View File

@ -5,29 +5,64 @@ args:
args:
sql: |
CREATE TABLE "automatic_comment_in_db" (
id serial primary key
id serial primary key,
name text not null
);
COMMENT ON TABLE "automatic_comment_in_db" IS 'What a great comment in the DB';
CREATE FUNCTION automatic_comment_in_db_upper_name(r "automatic_comment_in_db")
RETURNS text
LANGUAGE 'sql'
STABLE
AS $BODY$
SELECT upper(r.name)
$BODY$;
COMMENT ON FUNCTION "automatic_comment_in_db_upper_name"("automatic_comment_in_db") IS 'What a great comment on the function in the DB';
- type: run_sql
args:
sql: |
CREATE TABLE "automatic_no_comment_in_db" (
id serial primary key
id serial primary key,
name text not null
);
CREATE FUNCTION automatic_no_comment_in_db_upper_name(r "automatic_no_comment_in_db")
RETURNS text
LANGUAGE 'sql'
STABLE
AS $BODY$
SELECT upper(r.name)
$BODY$;
- type: run_sql
args:
sql: |
CREATE TABLE "explicit_comment_in_metadata" (
id serial primary key
id serial primary key,
name text not null
);
COMMENT ON TABLE "explicit_comment_in_metadata" IS 'Fantastic comment, so good, so hidden';
CREATE FUNCTION explicit_comment_in_metadata_upper_name(r "explicit_comment_in_metadata")
RETURNS text
LANGUAGE 'sql'
STABLE
AS $BODY$
SELECT upper(r.name)
$BODY$;
COMMENT ON FUNCTION "explicit_comment_in_metadata_upper_name"("explicit_comment_in_metadata") IS 'Fantastic comment on the function, so good, so hidden';
- type: run_sql
args:
sql: |
CREATE TABLE "explicit_no_comment_in_metadata" (
id serial primary key
id serial primary key,
name text not null
);
COMMENT ON TABLE "explicit_no_comment_in_metadata" IS 'This would be a great comment, but you can''t see it';
CREATE FUNCTION explicit_no_comment_in_metadata_upper_name(r "explicit_no_comment_in_metadata")
RETURNS text
LANGUAGE 'sql'
STABLE
AS $BODY$
SELECT upper(r.name)
$BODY$;
COMMENT ON FUNCTION "explicit_no_comment_in_metadata_upper_name"("explicit_no_comment_in_metadata") IS 'This would be a great comment on the function, but you can''t see it';

View File

@ -1,7 +1,11 @@
type: run_sql
args:
sql: |
DROP FUNCTION "automatic_comment_in_db_upper_name"("automatic_comment_in_db");
DROP TABLE "automatic_comment_in_db" cascade;
DROP FUNCTION "automatic_no_comment_in_db_upper_name"("automatic_no_comment_in_db");
DROP TABLE "automatic_no_comment_in_db" cascade;
DROP FUNCTION "explicit_comment_in_metadata_upper_name"("explicit_comment_in_metadata");
DROP TABLE "explicit_comment_in_metadata" cascade;
DROP FUNCTION "explicit_no_comment_in_metadata_upper_name"("explicit_no_comment_in_metadata");
DROP TABLE "explicit_no_comment_in_metadata" cascade;

View File

@ -4,18 +4,48 @@ args:
args:
table: automatic_comment_in_db
- type: pg_add_computed_field
args:
table: automatic_comment_in_db
name: upper_name
definition:
function: automatic_comment_in_db_upper_name
- type: pg_track_table
args:
table: automatic_no_comment_in_db
- type: pg_add_computed_field
args:
table: automatic_no_comment_in_db
name: upper_name
definition:
function: automatic_no_comment_in_db_upper_name
- type: pg_track_table
args:
table: explicit_comment_in_metadata
configuration:
comment: Such an explicit comment, wow
- type: pg_add_computed_field
args:
table: explicit_comment_in_metadata
name: upper_name
definition:
function: explicit_comment_in_metadata_upper_name
comment: Such an explicit function comment, wow
- type: pg_track_table
args:
table: explicit_no_comment_in_metadata
configuration:
comment: ""
- type: pg_add_computed_field
args:
table: explicit_no_comment_in_metadata
name: upper_name
definition:
function: explicit_no_comment_in_metadata_upper_name
comment: ""