Skip to content

Commit

Permalink
allow reusing Postgres scalars in custom types & actions (close #4125) (
Browse files Browse the repository at this point in the history
#4333)

* allow re-using Postgres scalars in custom types, close #4125

* add pytest tests

* update CHANGELOG.md

* add a doc pointer for reusable postgres scalars

* document the code, improve the CHANGELOG entry

As suggested by @lexi-lambda

* a bit more source code documentation, use WriterT to collect reused scalars

* Apply suggestions from code review

Co-Authored-By: Marion Schleifer <marion@hasura.io>

* improve doc for Postgres scalars in custom graphql types

* Add some more references to Note; fix Haddock syntax

Also a few very minor tweaks:
  * Use HashSet instead of [] more pervasively
  * Export execWriterT from Hasura.Prelude
  * Use pattern guards in multi-way if
  * Tweak a few names/comments

* Pull buildActions out of buildAndCollectInfo, use buildInfoMap

* Tweak wording in documentation

* incorporate changes in console code

* account Postgres scalars for action input arguments

-> Avoid unnecessary 'throw500' in making action schema

* Review changes

Co-authored-by: Marion Schleifer <marion@hasura.io>
Co-authored-by: Alexis King <lexi.lambda@gmail.com>
Co-authored-by: Vamshi Surabhi <0x777@users.noreply.github.com>
Co-authored-by: Aleksandra Sikora <ola.zxcvbnm@gmail.com>
  • Loading branch information
5 people authored Apr 15, 2020
1 parent dfc1f98 commit dc31b83
Show file tree
Hide file tree
Showing 22 changed files with 443 additions and 131 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The order and collapsed state of columns is now persisted across page navigation

### Bug fixes and improvements

- server: support reusing Postgres scalars in custom types (close #4125)
- cli: set_table_is_enum metadata type for squashing migrations (close #4394) (#4395)
- console: query support for actions (#4318)
- cli: query support for actions (#4318)
Expand Down
4 changes: 2 additions & 2 deletions console/src/components/Services/Actions/Common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const getActionTypes = (currentAction, allTypes) => {
const type = findType(allTypes, typename);
actionTypes[typename] = type;

if (type.fields) {
if (type && type.fields) {
type.fields.forEach(f => {
getDependentTypes(f.type);
if (f.arguments) {
Expand Down Expand Up @@ -268,7 +268,7 @@ export const getOverlappingTypeConfirmation = (
const action = otherActions[i];
const actionTypes = getActionTypes(action, allTypes);
actionTypes.forEach(t => {
if (typeCollisionMap[t.name]) return;
if (!t || typeCollisionMap[t.name]) return;
overlappingTypenames.forEach(ot => {
if (ot === t.name) {
typeCollisionMap[ot] = true;
Expand Down
33 changes: 17 additions & 16 deletions console/src/shared/utils/deriveAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ const deriveAction = (
const allHasuraTypes = clientSchema._typeMap;
const operationType = getOperationType(clientSchema, operation);

const isHasuraScalar = name => {
return isScalarType(allHasuraTypes[name]);
};

const actionArguments = [];
const newTypes = {};

Expand All @@ -128,7 +132,7 @@ const deriveAction = (
newType.name = typename;

if (isScalarType(type)) {
if (!inbuiltTypes[type.name]) {
if (!inbuiltTypes[type.name] && !allHasuraTypes[type.name]) {
newType.kind = 'scalar';
newTypes[typename] = newType;
}
Expand Down Expand Up @@ -156,7 +160,10 @@ const deriveAction = (
type: underLyingType,
wraps: fieldTypeWraps,
} = getUnderlyingType(tf.type);
if (inbuiltTypes[underLyingType.name]) {
if (
inbuiltTypes[underLyingType.name] ||
isHasuraScalar(underLyingType.name)
) {
_tf.type = wrapTypename(underLyingType.name, fieldTypeWraps);
} else {
_tf.type = wrapTypename(
Expand All @@ -177,7 +184,10 @@ const deriveAction = (
name: v.variable.name.value,
};
const argTypeMetadata = getAstTypeMetadata(v.type);
if (!inbuiltTypes[argTypeMetadata.typename]) {
if (
!inbuiltTypes[argTypeMetadata.typename] &&
!isHasuraScalar(argTypeMetadata.typename)
) {
const argTypename = prefixTypename(argTypeMetadata.typename);
generatedArg.type = wrapTypename(argTypename, argTypeMetadata.stack);
const typeInSchema = allHasuraTypes[argTypeMetadata.typename];
Expand Down Expand Up @@ -208,19 +218,10 @@ const deriveAction = (
outputTypeField => {
const fieldTypeMetadata = getUnderlyingType(outputTypeField.type);
if (isScalarType(fieldTypeMetadata.type)) {
if (inbuiltTypes[fieldTypeMetadata.type.name]) {
outputTypeFields[outputTypeField.name] = wrapTypename(
fieldTypeMetadata.type.name,
fieldTypeMetadata.wraps
);
} else {
const fieldTypename = prefixTypename(fieldTypeMetadata.type.name);
outputTypeFields[outputTypeField.name] = wrapTypename(
fieldTypename,
fieldTypeMetadata.wraps
);
handleType(fieldTypeMetadata.type, fieldTypename);
}
outputTypeFields[outputTypeField.name] = wrapTypename(
fieldTypeMetadata.type.name,
fieldTypeMetadata.wraps
);
}
}
);
Expand Down
1 change: 1 addition & 0 deletions console/src/shared/utils/sdlUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ ${enumValuesSdl.join('\n')}
};

const getTypeSdl = type => {
if (!type) return '';
switch (type.kind) {
case 'scalar':
return getScalarTypeSdl(type);
Expand Down
17 changes: 16 additions & 1 deletion docs/graphql/manual/actions/types/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ a scalar called ``Date``, you can define it like.
These scalars can be used as arguments of the mutation or as fields of object
types and input types.

.. admonition:: Postgres scalars

Postgres base types are implicitly made available as GraphQL scalars; there
is no need to declare them separately. For example, in the definition

.. code-block:: graphql
type User {
id: uuid!
name: String!
location: geography
}
the ``uuid`` and ``geography`` types are assumed to refer to Postgres
scalars (assuming no other definition for them is provided).

Enum types
----------

Expand All @@ -165,4 +181,3 @@ This means that wherever we use the type ``Color`` in our schema, we expect it
to be exactly one of RED, GREEN, or BLUE.

`See reference <https://graphql.org/learn/schema/#enumeration-types>`__

2 changes: 1 addition & 1 deletion server/src-lib/Hasura/GraphQL/Resolve/Action.hs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ asyncActionsProcessor cacheRef pgPool httpManager = forever $ do
Nothing -> return ()
Just actionInfo -> do
let definition = _aiDefinition actionInfo
outputFields = _aiOutputFields actionInfo
outputFields = getActionOutputFields $ _aiOutputObject actionInfo
webhookUrl = _adHandler definition
forwardClientHeaders = _adForwardClientHeaders definition
confHeaders = _adHeaders definition
Expand Down
6 changes: 3 additions & 3 deletions server/src-lib/Hasura/GraphQL/Schema.hs
Original file line number Diff line number Diff line change
Expand Up @@ -720,11 +720,11 @@ noFilter = annBoolExpTrue

mkGCtxMap
:: forall m. (MonadError QErr m)
=> AnnotatedObjects -> TableCache -> FunctionCache -> ActionCache -> m GCtxMap
mkGCtxMap annotatedObjects tableCache functionCache actionCache = do
=> TableCache -> FunctionCache -> ActionCache -> m GCtxMap
mkGCtxMap tableCache functionCache actionCache = do
typesMapL <- mapM (mkGCtxMapTable tableCache functionCache) $
filter (tableFltr . _tiCoreInfo) $ Map.elems tableCache
actionsSchema <- mkActionsSchema annotatedObjects actionCache
let actionsSchema = mkActionsSchema actionCache
typesMap <- combineTypes actionsSchema typesMapL
let gCtxMap = flip Map.map typesMap $
\(ty, flds, insCtxMap) -> mkGCtx ty flds insCtxMap
Expand Down
72 changes: 32 additions & 40 deletions server/src-lib/Hasura/GraphQL/Schema/Action.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Hasura.GraphQL.Schema.Action
) where

import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import qualified Language.GraphQL.Draft.Syntax as G

import Data.Coerce (coerce)
Expand Down Expand Up @@ -68,14 +69,14 @@ mkMutationField actionName actionInfo definitionList =
ActionSynchronous ->
ActionExecutionSyncWebhook $ SyncActionExecutionContext actionName
(_adOutputType definition)
(_aiOutputFields actionInfo)
(getActionOutputFields $ _aiOutputObject actionInfo)
definitionList
(_adHandler definition)
(_adHeaders definition)
(_adForwardClientHeaders definition)
ActionAsynchronous -> ActionExecutionAsync

description = mkDescriptionWith (PGDescription <$> (_aiComment actionInfo)) $
description = mkDescriptionWith (PGDescription <$> _aiComment actionInfo) $
"perform the action: " <>> actionName

fieldInfo =
Expand Down Expand Up @@ -123,22 +124,21 @@ mkQueryField actionName comment definition definitionList =
idDescription = G.Description $ "id of the action: " <>> actionName

mkActionFieldsAndTypes
:: (QErrM m)
=> ActionInfo
-> AnnotatedObjectType
:: ActionInfo
-> ActionPermissionInfo
-> m ( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
-> ( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
-- context, field, response type info
, (ActionExecutionContext, ObjFldInfo) -- mutation field
, FieldMap
)
mkActionFieldsAndTypes actionInfo annotatedOutputType permission =
return ( mkQueryField actionName comment definition definitionList
, mkMutationField actionName actionInfo definitionList
, fieldMap
)
mkActionFieldsAndTypes actionInfo permission =
( mkQueryField actionName comment definition definitionList
, mkMutationField actionName actionInfo definitionList
, fieldMap
)
where
actionName = _aiName actionInfo
annotatedOutputType = _aiOutputObject actionInfo
definition = _aiDefinition actionInfo
roleName = _apiRole permission
comment = _aiComment actionInfo
Expand Down Expand Up @@ -220,46 +220,38 @@ mkActionFieldsAndTypes actionInfo annotatedOutputType permission =
G.getBaseType $ unGraphQLType $ _adOutputType $ _aiDefinition actionInfo

mkActionSchemaOne
:: (QErrM m)
=> AnnotatedObjects
-> ActionInfo
-> m (Map.HashMap RoleName
( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
, (ActionExecutionContext, ObjFldInfo)
, FieldMap
)
:: ActionInfo
-> Map.HashMap RoleName
( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
, (ActionExecutionContext, ObjFldInfo)
, FieldMap
)
mkActionSchemaOne annotatedObjects actionInfo = do
annotatedOutputType <- onNothing
(Map.lookup (ObjectTypeName actionOutputBaseType) annotatedObjects) $
throw500 $ "missing annotated type for: " <> showNamedTy actionOutputBaseType
forM permissions $ \permission ->
mkActionFieldsAndTypes actionInfo annotatedOutputType permission
mkActionSchemaOne actionInfo =
flip Map.map permissions $ \permission ->
mkActionFieldsAndTypes actionInfo permission
where
adminPermission = ActionPermissionInfo adminRole
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
actionOutputBaseType =
G.getBaseType $ unGraphQLType $ _adOutputType $ _aiDefinition actionInfo

mkActionsSchema
:: (QErrM m)
=> AnnotatedObjects
-> ActionCache
-> m (Map.HashMap RoleName (RootFields, TyAgg))
mkActionsSchema annotatedObjects =
foldM
:: ActionCache
-> Map.HashMap RoleName (RootFields, TyAgg)
mkActionsSchema =
foldl'
(\aggregate actionInfo ->
Map.foldrWithKey f aggregate <$>
mkActionSchemaOne annotatedObjects actionInfo
Map.foldrWithKey (accumulate (_aiPgScalars actionInfo)) aggregate $ mkActionSchemaOne actionInfo
)
mempty
where
-- we'll need to add uuid and timestamptz for actions
newRoleState = (mempty, addScalarToTyAgg PGJSON $
addScalarToTyAgg PGTimeStampTZ $
addScalarToTyAgg PGUUID mempty)
f roleName (queryFieldM, mutationField, fields) =
Map.alter (Just . addToState . fromMaybe newRoleState) roleName
mkNewRoleState pgScalars =
( mempty
, foldr addScalarToTyAgg mempty $
pgScalars <> Set.fromList [PGJSON, PGTimeStampTZ, PGUUID]
)

accumulate pgScalars roleName (queryFieldM, mutationField, fields) =
Map.alter (Just . addToState . fromMaybe (mkNewRoleState pgScalars)) roleName
where
addToState = case queryFieldM of
Just (fldCtx, fldDefinition, responseTypeInfo) ->
Expand Down
16 changes: 10 additions & 6 deletions server/src-lib/Hasura/GraphQL/Schema/CustomTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ buildObjectTypeInfo roleName annotatedObjectType =
\(TypeRelationship name ty remoteTableInfo _) ->
if isJust (getSelectPermissionInfoM remoteTableInfo roleName) ||
roleName == adminRole
then Just (relationshipToFieldInfo name ty $ _tciName $ _tiCoreInfo $ remoteTableInfo)
then Just (relationshipToFieldInfo name ty $ _tciName $ _tiCoreInfo remoteTableInfo)
else Nothing
where
relationshipToFieldInfo name relTy remoteTableName =
Expand Down Expand Up @@ -116,14 +116,18 @@ annotateObjectType tableCache nonObjectTypeMap objectDefinition = do

buildCustomTypesSchemaPartial
:: (QErrM m)
=> TableCache -> CustomTypes -> m (NonObjectTypeMap, AnnotatedObjects)
buildCustomTypesSchemaPartial tableCache customTypes = do
=> TableCache
-> CustomTypes
-> HashSet PGScalarType
-- ^ Postgres base types used in the custom type definitions;
-- see Note [Postgres scalars in custom types].
-> m (NonObjectTypeMap, AnnotatedObjects)
buildCustomTypesSchemaPartial tableCache customTypes pgScalars = do
let typeInfos =
map (VT.TIEnum . convertEnumDefinition) enumDefinitions <>
-- map (VT.TIObj . convertObjectDefinition) objectDefinitions <>
map (VT.TIInpObj . convertInputObjectDefinition) inputObjectDefinitions <>
map (VT.TIScalar . convertScalarDefinition) scalarDefinitions
-- <> defaultTypes
map (VT.TIScalar . convertScalarDefinition) scalarDefinitions <>
map (VT.TIScalar . VT.mkHsraScalarTyInfo) (toList pgScalars)
nonObjectTypeMap = NonObjectTypeMap $ mapFromL VT.getNamedTy typeInfos

annotatedObjectTypes <- mapFromL (_otdName . _aotDefinition) <$>
Expand Down
3 changes: 2 additions & 1 deletion server/src-lib/Hasura/Prelude.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import Control.Monad.Fail as M (MonadFail)
import Control.Monad.Identity as M
import Control.Monad.Reader as M
import Control.Monad.State.Strict as M
import Control.Monad.Writer.Strict as M (MonadWriter (..), WriterT (..))
import Control.Monad.Writer.Strict as M (MonadWriter (..), WriterT (..),
execWriterT, runWriterT)
import Data.Align as M (Align (align, alignWith))
import Data.Align.Key as M (AlignWithKey (..))
import Data.Bool as M (bool)
Expand Down
Loading

0 comments on commit dc31b83

Please sign in to comment.