server: customize tracking tables with a custom name (#5719)

https://github.com/hasura/graphql-engine/pull/5719
This commit is contained in:
Karthikeyan Chinnakonda 2020-10-29 18:18:45 +05:30 committed by GitHub
parent 562b6ac43a
commit 2cb08a89cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1392 additions and 101 deletions

View File

@ -85,6 +85,7 @@ This release contains the [PDV refactor (#4111)](https://github.com/hasura/graph
**NOTE:** If you have event triggers with names greater than 42 chars, then you should update their names to avoid running into Postgres identifier limit bug (#5786)
- server: validate remote schema queries (fixes #4143)
- server: fix issue with tracking custom functions that return `SETOF` materialized view (close #5294) (#5945)
- server: introduce optional custom table name in table configuration to track the table according to the custom name. The `set_table_custom_fields` API has been deprecated, A new API `set_table_customization` has been added to set the configuration. (#3811)
- server: allow remote relationships with union, interface and enum type fields as well (fixes #5875) (#6080)
- console: allow user to cascade Postgres dependencies when dropping Postgres objects (close #5109) (#5248)
- console: mark inconsistent remote schemas in the UI (close #5093) (#5181)

View File

@ -104,10 +104,15 @@ The various types of queries are listed in the following table:
- 2
- Add a table/view with configuration
* - :ref:`set_table_custom_fields <set_table_custom_fields>`
* - :ref:`set_table_customization <set_table_customization>`
- :ref:`set_table_customization_args <set_table_customization_syntax>`
- 1
- Set table customization of an already tracked table
* - :ref:`set_table_custom_fields <set_table_custom_fields>` (deprecated)
- :ref:`set_table_custom_fields_args <set_table_custom_fields_args_syntax>`
- 2
- Set custom fields to an already tracked table
- Set custom fields to an already tracked table (deprecated)
* - :ref:`untrack_table`
- :ref:`untrack_table_args <untrack_table_syntax>`

View File

@ -149,6 +149,45 @@ Add a table/view ``author``:
}
}
A table can be tracked with a ``custom name``. This can be useful when a table
name is not GraphQL compliant, like ``Users Address``. A ``custom name`` like
``users_address`` will complement the ``"Users Address"``
table, so that it can be added to the GraphQL schema.
.. code-block:: http
POST /v1/query HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "track_table",
"version": 2,
"args": {
"table": "Author Details",
"configuration": {
"custom_name": "author_details"
}
}
}
The GraphQL nodes and typenames
that are generated will be according to the ``identifier``. For example, in this case,
the nodes generated will be:
- ``users_address``
- ``users_address_one``
- ``users_address_aggregate``
- ``insert_users_address``
- ``insert_users_address_one``
- ``update_users_address``
- ``update_users_address_by_pk``
- ``delete_users_address``
- ``delete_users_address_by_pk``
.. note::
graphql-engine requires the constraint names (if any) of a table to be GraphQL `compliant <https://spec.graphql.org/June2018/#sec-Names>`__ in order to be able to track it.
.. _track_table_args_syntax_v2:
Args syntax
@ -182,6 +221,11 @@ Table Config
- Required
- Schema
- Description
* - custom_name
- false
- ``String``
- Customise the ``<table-name>`` with the provided custom name value
. The GraphQL nodes for the table will be generated according to the custom name.
* - custom_root_fields
- false
- :ref:`Custom Root Fields <custom_root_fields>`
@ -242,13 +286,19 @@ Custom Root Fields
.. _set_table_custom_fields:
set_table_custom_fields
-----------------------
set_table_custom_fields (deprecated)
------------------------------------
``set_table_custom_fields`` has been deprecated. Use the
:ref:`set_table_configuration <set_table_configuration>` API to set the custom
table fields.
``set_table_custom_fields`` in version ``2`` sets the custom root fields and
custom column names of already tracked table. This will **replace** the already
present custom fields configuration.
Set custom fields for table/view ``author``:
.. code-block:: http
@ -304,6 +354,70 @@ Args syntax
- :ref:`CustomColumnNames`
- Customise the column fields
.. _set_table_customization:
set_table_customization
-----------------------
``set_table_customization`` allows to customize any given table with
a custom name, custom root fields and custom column names of an already tracked
table. This will **replace** the already present customization.
:ref:`set_table_custom_fields <set_table_custom_fields>` has been deprecated in
favour of this API.
Set configuration for table/view ``author``:
.. code-block:: http
POST /v1/query HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "set_table_customization",
"args": {
"table": "author_details",
"configuration": {
"identifier": "author",
"custom_root_fields": {
"select": "Authors",
"select_by_pk": "Author",
"select_aggregate": "AuthorAggregate",
"insert": "AddAuthors",
"insert_one":"AddAuthor",
"update": "UpdateAuthors",
"update_by_pk": "UpdateAuthor",
"delete": "DeleteAuthors",
"delete_by_pk": "DeleteAuthor"
},
"custom_column_names": {
"id": "authorId"
}
}
}
}
.. _set_table_customization_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - table
- true
- :ref:`TableName <TableName>`
- Name of the table
* - configuration
- false
- :ref:`TableConfig <table_config>`
- Configuration for the table/view
.. _untrack_table:

View File

@ -38,6 +38,7 @@ module Hasura.Backends.Postgres.SQL.Types
, qualifiedObjectToText
, snakeCaseQualifiedObject
, qualifiedObjectToName
, isGraphQLCompliantTableName
, PGScalarType(..)
, WithScalarType(..)
@ -241,6 +242,9 @@ qualifiedObjectToName objectName = do
"cannot include " <> objectName <<> " in the GraphQL schema because " <> textName
<<> " is not a valid GraphQL identifier"
isGraphQLCompliantTableName :: ToTxt a => QualifiedObject a -> Bool
isGraphQLCompliantTableName = isJust . G.mkName . snakeCaseQualifiedObject
type QualifiedTable = QualifiedObject TableName
type QualifiedFunction = QualifiedObject FunctionName

View File

@ -5,6 +5,7 @@ import Hasura.Prelude
import qualified Data.HashMap.Strict as Map
import qualified Language.Haskell.TH as TH
import qualified Language.GraphQL.Draft.Syntax as G
import Data.Has
import Data.Parser.JSONPath
@ -16,7 +17,7 @@ import Type.Reflection (Typeable)
import Hasura.Backends.Postgres.SQL.Types
import {-# SOURCE #-} Hasura.GraphQL.Parser.Internal.Parser
import Hasura.RQL.Types.Error
import Hasura.RQL.Types.Table (TableCache, TableInfo)
import Hasura.RQL.Types.Table
import Hasura.SQL.Backend
import Hasura.Session (RoleName)
@ -124,6 +125,23 @@ askTableInfo tableName = do
-- supposed to ensure that all dependencies are resolved.
tableInfo `onNothing` throw500 ("askTableInfo: no info for " <>> tableName)
-- | Helper function to get the table GraphQL name. A table may have an
-- identifier configured with it. When the identifier exists, the GraphQL nodes
-- that are generated according to the identifier. For example: Let's say,
-- we have a table called `users address`, the name of the table is not GraphQL
-- compliant so we configure the table with a GraphQL compliant name,
-- say `users_address`
-- The generated top-level nodes of this table will be like `users_address`,
-- `insert_users_address` etc
getTableGQLName
:: MonadTableInfo r m
=> QualifiedTable
-> m G.Name
getTableGQLName table = do
tableInfo <- askTableInfo table
let tableCustomName = _tcCustomName . _tciCustomConfig . _tiCoreInfo $ tableInfo
maybe (qualifiedObjectToName table) pure tableCustomName
-- | A wrapper around 'memoizeOn' that memoizes a function by using its argument
-- as the key.
memoize

View File

@ -73,7 +73,17 @@ buildGQLContext =
tableFilter = not . isSystemDefined . _tciSystemDefined
functionFilter = not . isSystemDefined . fiSystemDefined
graphQLTableFilter tableName tableInfo =
-- either the table name should be GraphQL compliant
-- or it should have a GraphQL custom name set with it
isGraphQLCompliantTableName tableName
|| (isJust . _tcCustomName . _tciCustomConfig . _tiCoreInfo $ tableInfo)
validTables = Map.filter (tableFilter . _tiCoreInfo) allTables
-- Only tables that have GraphQL compliant names will be added to the schema.
-- We allow tables which don't have GraphQL compliant names, so that RQL CRUD
-- operations can be performed on them
graphQLTables = Map.filterWithKey graphQLTableFilter validTables
validFunctions = Map.elems $ Map.filter functionFilter allFunctions
allActionInfos = Map.elems allActions
@ -87,12 +97,12 @@ buildGQLContext =
SQLGenCtx{ stringifyNum } <- askSQLGenCtx
let gqlContext =
(,)
<$> queryWithIntrospection (Set.fromMap $ validTables $> ())
<$> queryWithIntrospection (Set.fromMap $ graphQLTables $> ())
validFunctions mempty mempty
allActionInfos nonObjectCustomTypes
<*> mutation (Set.fromMap $ validTables $> ()) mempty
<*> mutation (Set.fromMap $ graphQLTables $> ()) mempty
allActionInfos nonObjectCustomTypes
flip runReaderT (adminRoleName, validTables, Frontend, QueryContext stringifyNum queryType queryRemotesMap) $
flip runReaderT (adminRoleName, graphQLTables, Frontend, QueryContext stringifyNum queryType queryRemotesMap) $
P.runSchemaT gqlContext
-- build the admin context so that we can check against name clashes with remotes
@ -164,19 +174,19 @@ buildGQLContext =
queryRemotes = concatMap (piQuery . snd) remotes
mutationRemotes = concatMap (concat . piMutation . snd) remotes
queryHasuraOrRelay = case queryType of
QueryHasura -> queryWithIntrospection (Set.fromMap $ validTables $> ())
QueryHasura -> queryWithIntrospection (Set.fromMap $ graphQLTables $> ())
validFunctions queryRemotes mutationRemotes
allActionInfos nonObjectCustomTypes
QueryRelay -> relayWithIntrospection (Set.fromMap $ validTables $> ()) validFunctions
QueryRelay -> relayWithIntrospection (Set.fromMap $ graphQLTables $> ()) validFunctions
buildContextForRoleAndScenario :: RoleName -> Scenario -> m GQLContext
buildContextForRoleAndScenario roleName scenario = do
SQLGenCtx{ stringifyNum } <- askSQLGenCtx
let gqlContext = GQLContext
<$> (finalizeParser <$> queryHasuraOrRelay)
<*> (fmap finalizeParser <$> mutation (Set.fromList $ Map.keys validTables) mutationRemotes
<*> (fmap finalizeParser <$> mutation (Set.fromList $ Map.keys graphQLTables) mutationRemotes
allActionInfos nonObjectCustomTypes)
flip runReaderT (roleName, validTables, scenario, QueryContext stringifyNum queryType queryRemotesMap) $
flip runReaderT (roleName, graphQLTables, scenario, QueryContext stringifyNum queryType queryRemotesMap) $
P.runSchemaT gqlContext
buildContextForRole :: RoleName -> m (RoleContext GQLContext)
@ -216,14 +226,14 @@ query' allTables allFunctions allRemotes allActions nonObjectCustomTypes = do
selectPerms <- tableSelectPermissions table
customRootFields <- _tcCustomRootFields . _tciCustomConfig . _tiCoreInfo <$> askTableInfo table
for selectPerms \perms -> do
displayName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
let fieldsDesc = G.Description $ "fetch data from the table: " <>> table
aggName = displayName <> $$(G.litName "_aggregate")
aggName = tableGQLName <> $$(G.litName "_aggregate")
aggDesc = G.Description $ "fetch aggregated fields from the table: " <>> table
pkName = displayName <> $$(G.litName "_by_pk")
pkName = tableGQLName <> $$(G.litName "_by_pk")
pkDesc = G.Description $ "fetch data from the table: " <> table <<> " using primary key columns"
catMaybes <$> sequenceA
[ requiredFieldParser (RFDB . QDBSimple) $ selectTable table (fromMaybe displayName $ _tcrfSelect customRootFields) (Just fieldsDesc) perms
[ requiredFieldParser (RFDB . QDBSimple) $ selectTable table (fromMaybe tableGQLName $ _tcrfSelect customRootFields) (Just fieldsDesc) perms
, mapMaybeFieldParser (RFDB . QDBPrimaryKey) $ selectTableByPk table (fromMaybe pkName $ _tcrfSelectByPk customRootFields) (Just pkDesc) perms
, mapMaybeFieldParser (RFDB . QDBAggregation) $ selectTableAggregate table (fromMaybe aggName $ _tcrfSelectAggregate customRootFields) (Just aggDesc) perms
]
@ -232,12 +242,12 @@ query' allTables allFunctions allRemotes allActions nonObjectCustomTypes = do
functionName = fiName function
selectPerms <- tableSelectPermissions targetTable
for selectPerms \perms -> do
displayName <- qualifiedObjectToName functionName
tableGQLName <- qualifiedObjectToName functionName
let functionDesc = G.Description $ "execute function " <> functionName <<> " which returns " <>> targetTable
aggName = displayName <> $$(G.litName "_aggregate")
aggName = tableGQLName <> $$(G.litName "_aggregate")
aggDesc = G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> targetTable
catMaybes <$> sequenceA
[ requiredFieldParser (RFDB . QDBSimple) $ selectFunction function displayName (Just functionDesc) perms
[ requiredFieldParser (RFDB . QDBSimple) $ selectFunction function tableGQLName (Just functionDesc) perms
, mapMaybeFieldParser (RFDB . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms
]
actionParsers <- for allActions $ \actionInfo ->
@ -275,8 +285,8 @@ relayQuery' allTables allFunctions = do
pkeyColumns <- MaybeT $ (^? tiCoreInfo.tciPrimaryKey._Just.pkColumns)
<$> askTableInfo table
selectPerms <- MaybeT $ tableSelectPermissions table
displayName <- qualifiedObjectToName table
let fieldName = displayName <> $$(G.litName "_connection")
tableGQLName <- getTableGQLName table
let fieldName = tableGQLName <> $$(G.litName "_connection")
fieldDesc = Just $ G.Description $ "fetch data from the table: " <>> table
lift $ selectTableConnection table fieldName fieldDesc pkeyColumns selectPerms
@ -287,8 +297,8 @@ relayQuery' allTables allFunctions = do
pkeyColumns <- MaybeT $ (^? tiCoreInfo.tciPrimaryKey._Just.pkColumns)
<$> askTableInfo returnTable
selectPerms <- MaybeT $ tableSelectPermissions returnTable
displayName <- qualifiedObjectToName functionName
let fieldName = displayName <> $$(G.litName "_connection")
tableGQLName <- qualifiedObjectToName functionName
let fieldName = tableGQLName <> $$(G.litName "_connection")
fieldDesc = Just $ G.Description $ "execute function " <> functionName
<<> " which returns " <>> returnTable
lift $ selectFunctionConnection function fieldName fieldDesc pkeyColumns selectPerms
@ -485,7 +495,7 @@ mutation
mutation allTables allRemotes allActions nonObjectCustomTypes = do
mutationParsers <- for (toList allTables) \table -> do
tableCoreInfo <- _tiCoreInfo <$> askTableInfo table
displayName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
tablePerms <- tablePermissions table
for tablePerms \permissions -> do
let customRootFields = _tcCustomRootFields $ _tciCustomConfig tableCoreInfo
@ -500,9 +510,9 @@ mutation allTables allRemotes allActions nonObjectCustomTypes = do
then Nothing
else return insertPermission
inserts <- fmap join $ whenMaybe (isMutable viIsInsertable viewInfo) $ for scenarioInsertPermissionM \insertPerms -> do
let insertName = $$(G.litName "insert_") <> displayName
let insertName = $$(G.litName "insert_") <> tableGQLName
insertDesc = G.Description $ "insert data into the table: " <>> table
insertOneName = $$(G.litName "insert_") <> displayName <> $$(G.litName "_one")
insertOneName = $$(G.litName "insert_") <> tableGQLName <> $$(G.litName "_one")
insertOneDesc = G.Description $ "insert a single row into the table: " <>> table
insert <- insertIntoTable table (fromMaybe insertName $ _tcrfInsert customRootFields) (Just insertDesc) insertPerms selectPerms (_permUpd permissions)
-- select permissions are required for InsertOne: the
@ -514,9 +524,9 @@ mutation allTables allRemotes allActions nonObjectCustomTypes = do
pure $ fmap (RFDB . MDBInsert) insert : maybe [] (pure . fmap (RFDB . MDBInsert)) insertOne
updates <- fmap join $ whenMaybe (isMutable viIsUpdatable viewInfo) $ for (_permUpd permissions) \updatePerms -> do
let updateName = $$(G.litName "update_") <> displayName
let updateName = $$(G.litName "update_") <> tableGQLName
updateDesc = G.Description $ "update data of the table: " <>> table
updateByPkName = $$(G.litName "update_") <> displayName <> $$(G.litName "_by_pk")
updateByPkName = $$(G.litName "update_") <> tableGQLName <> $$(G.litName "_by_pk")
updateByPkDesc = G.Description $ "update single row of the table: " <>> table
update <- updateTable table (fromMaybe updateName $ _tcrfUpdate customRootFields) (Just updateDesc) updatePerms selectPerms
-- likewise; furthermore, primary keys can only be tested in
@ -527,9 +537,9 @@ mutation allTables allRemotes allActions nonObjectCustomTypes = do
pure $ fmap (RFDB . MDBUpdate) <$> catMaybes [update, updateByPk]
deletes <- fmap join $ whenMaybe (isMutable viIsDeletable viewInfo) $ for (_permDel permissions) \deletePerms -> do
let deleteName = $$(G.litName "delete_") <> displayName
let deleteName = $$(G.litName "delete_") <> tableGQLName
deleteDesc = G.Description $ "delete data from the table: " <>> table
deleteByPkName = $$(G.litName "delete_") <> displayName <> $$(G.litName "_by_pk")
deleteByPkName = $$(G.litName "delete_") <> tableGQLName <> $$(G.litName "_by_pk")
deleteByPkDesc = G.Description $ "delete single row from the table: " <>> table
delete <- deleteFromTable table (fromMaybe deleteName $ _tcrfDelete customRootFields) (Just deleteDesc) deletePerms selectPerms

View File

@ -36,7 +36,8 @@ boolExp
-> Maybe (SelPermInfo 'Postgres)
-> m (Parser 'Input n (AnnBoolExp 'Postgres UnpreparedValue))
boolExp table selectPermissions = memoizeOn 'boolExp table $ do
name <- qualifiedObjectToName table <&> (<> $$(G.litName "_bool_exp"))
tableGQLName <- getTableGQLName table
let name = tableGQLName <> $$(G.litName "_bool_exp")
let description = G.Description $
"Boolean expression to filter rows from the table " <> table <<>
". All fields are combined with a logical 'AND'."

View File

@ -114,7 +114,7 @@ tableFieldsInput
-> InsPermInfo 'Postgres -- ^ insert permissions of the table
-> m (Parser 'Input n (AnnInsObj 'Postgres UnpreparedValue))
tableFieldsInput table insertPerms = memoizeOn 'tableFieldsInput table do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
allFields <- _tciFieldInfoMap . _tiCoreInfo <$> askTableInfo table
objectFields <- catMaybes <$> for (Map.elems allFields) \case
FIComputedField _ -> pure Nothing
@ -144,7 +144,7 @@ tableFieldsInput table insertPerms = memoizeOn 'tableFieldsInput table do
pure $ P.fieldOptional relFieldName Nothing parser <&> \arrRelIns -> do
rel <- join arrRelIns
Just $ AnnInsObj [] [] [RelIns rel relationshipInfo | not $ null $ _aiInsObj rel]
let objectName = tableName <> $$(G.litName "_insert_input")
let objectName = tableGQLName <> $$(G.litName "_insert_input")
objectDesc = G.Description $ "input type for inserting data into table " <>> table
pure $ P.object objectName (Just objectDesc) $ catMaybes <$> sequenceA objectFields
<&> mconcat
@ -159,12 +159,12 @@ objectRelationshipInput
-> m (Parser 'Input n (SingleObjIns 'Postgres UnpreparedValue))
objectRelationshipInput table insertPerms selectPerms updatePerms =
memoizeOn 'objectRelationshipInput table do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
columns <- tableColumns table
objectParser <- tableFieldsInput table insertPerms
conflictParser <- fmap join $ sequenceA $ conflictObject table selectPerms <$> updatePerms
let objectName = $$(G.litName "data")
inputName = tableName <> $$(G.litName "_obj_rel_insert_input")
inputName = tableGQLName <> $$(G.litName "_obj_rel_insert_input")
inputDesc = G.Description $ "input type for inserting object relation for remote table " <>> table
inputParser = do
conflictClause <- mkConflictClause conflictParser
@ -182,12 +182,12 @@ arrayRelationshipInput
-> m (Parser 'Input n (MultiObjIns 'Postgres UnpreparedValue))
arrayRelationshipInput table insertPerms selectPerms updatePerms =
memoizeOn 'arrayRelationshipInput table do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
columns <- tableColumns table
objectParser <- tableFieldsInput table insertPerms
conflictParser <- fmap join $ sequenceA $ conflictObject table selectPerms <$> updatePerms
let objectsName = $$(G.litName "data")
inputName = tableName <> $$(G.litName "_arr_rel_insert_input")
inputName = tableGQLName <> $$(G.litName "_arr_rel_insert_input")
inputDesc = G.Description $ "input type for inserting array relation for remote table " <>> table
inputParser = do
conflictClause <- mkConflictClause conflictParser
@ -224,12 +224,12 @@ conflictObject
-> UpdPermInfo 'Postgres
-> m (Maybe (Parser 'Input n (RQL.ConflictClauseP1 'Postgres UnpreparedValue)))
conflictObject table selectPerms updatePerms = runMaybeT $ do
tableName <- lift $ qualifiedObjectToName table
tableGQLName <- getTableGQLName table
columnsEnum <- MaybeT $ tableUpdateColumnsEnum table updatePerms
constraints <- MaybeT $ tciUniqueOrPrimaryKeyConstraints . _tiCoreInfo <$> askTableInfo table
constraintParser <- lift $ conflictConstraint constraints table
whereExpParser <- lift $ boolExp table selectPerms
let objectName = tableName <> $$(G.litName "_on_conflict")
let objectName = tableGQLName <> $$(G.litName "_on_conflict")
objectDesc = G.Description $ "on conflict condition type for table " <>> table
constraintName = $$(G.litName "constraint")
columnsName = $$(G.litName "update_columns")
@ -251,18 +251,17 @@ conflictConstraint
-> QualifiedTable
-> m (Parser 'Both n ConstraintName)
conflictConstraint constraints table = memoizeOn 'conflictConstraint table $ do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
constraintEnumValues <- for constraints \constraint -> do
name <- textToName $ getConstraintTxt $ _cName constraint
pure ( P.mkDefinition name (Just "unique or primary key constraint") P.EnumValueInfo
, _cName constraint
)
let enumName = tableName <> $$(G.litName "_constraint")
let enumName = tableGQLName <> $$(G.litName "_constraint")
enumDesc = G.Description $ "unique or primary key constraints on table " <>> table
pure $ P.enum enumName (Just enumDesc) constraintEnumValues
-- update
-- | Construct a root field, normally called update_tablename, that can be used
@ -300,14 +299,14 @@ updateTableByPk
-> SelPermInfo 'Postgres -- ^ select permissions of the table
-> m (Maybe (FieldParser n (RQL.AnnUpdG 'Postgres UnpreparedValue)))
updateTableByPk table fieldName description updatePerms selectPerms = runMaybeT $ do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
columns <- lift $ tableSelectColumns table selectPerms
pkArgs <- MaybeT $ primaryKeysArguments table selectPerms
opArgs <- MaybeT $ updateOperators table updatePerms
selection <- lift $ tableSelectionSet table selectPerms
let pkFieldName = $$(G.litName "pk_columns")
pkObjectName = tableName <> $$(G.litName "_pk_columns_input")
pkObjectDesc = G.Description $ "primary key columns input for table: " <> G.unName tableName
pkObjectName = tableGQLName <> $$(G.litName "_pk_columns_input")
pkObjectDesc = G.Description $ "primary key columns input for table: " <> G.unName tableGQLName
argsParser = do
operators <- opArgs
primaryKeys <- P.field pkFieldName Nothing $ P.object pkObjectName (Just pkObjectDesc) pkArgs
@ -344,40 +343,40 @@ updateOperators
-> UpdPermInfo 'Postgres -- ^ update permissions of the table
-> m (Maybe (InputFieldsParser n [(PGCol, RQL.UpdOpExpG UnpreparedValue)]))
updateOperators table updatePermissions = do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
columns <- tableUpdateColumns table updatePermissions
let numericCols = onlyNumCols columns
jsonCols = onlyJSONBCols columns
parsers <- catMaybes <$> sequenceA
[ updateOperator tableName $$(G.litName "_set")
[ updateOperator tableGQLName $$(G.litName "_set")
columnParser RQL.UpdSet columns
"sets the columns of the filtered rows to the given values"
(G.Description $ "input type for updating data in table " <>> table)
, updateOperator tableName $$(G.litName "_inc")
, updateOperator tableGQLName $$(G.litName "_inc")
columnParser RQL.UpdInc numericCols
"increments the numeric columns with given value of the filtered values"
(G.Description $"input type for incrementing numeric columns in table " <>> table)
, let desc = "prepend existing jsonb value of filtered columns with new jsonb value"
in updateOperator tableName $$(G.litName "_prepend")
in updateOperator tableGQLName $$(G.litName "_prepend")
columnParser RQL.UpdPrepend jsonCols desc desc
, let desc = "append existing jsonb value of filtered columns with new jsonb value"
in updateOperator tableName $$(G.litName "_append")
in updateOperator tableGQLName $$(G.litName "_append")
columnParser RQL.UpdAppend jsonCols desc desc
, let desc = "delete key/value pair or string element. key/value pairs are matched based on their key value"
in updateOperator tableName $$(G.litName "_delete_key")
in updateOperator tableGQLName $$(G.litName "_delete_key")
nullableTextParser RQL.UpdDeleteKey jsonCols desc desc
, let desc = "delete the array element with specified index (negative integers count from the end). "
<> "throws an error if top level container is not an array"
in updateOperator tableName $$(G.litName "_delete_elem")
in updateOperator tableGQLName $$(G.litName "_delete_elem")
nonNullableIntParser RQL.UpdDeleteElem jsonCols desc desc
, let desc = "delete the field or element with specified path (for JSON arrays, negative integers count from the end)"
in updateOperator tableName $$(G.litName "_delete_at_path")
in updateOperator tableGQLName $$(G.litName "_delete_at_path")
(fmap P.list . nonNullableTextParser) RQL.UpdDeleteAtPath jsonCols desc desc
]
whenMaybe (not $ null parsers) do
@ -414,7 +413,7 @@ updateOperators table updatePermissions = do
-> G.Description
-> G.Description
-> m (Maybe (Text, InputFieldsParser n (Maybe [(PGCol, RQL.UpdOpExpG UnpreparedValue)])))
updateOperator tableName opName mkParser updOpExp columns opDesc objDesc =
updateOperator tableGQLName opName mkParser updOpExp columns opDesc objDesc =
whenMaybe (not $ null columns) do
fields <- for columns \columnInfo -> do
let fieldName = pgiName columnInfo
@ -422,7 +421,7 @@ updateOperators table updatePermissions = do
fieldParser <- mkParser columnInfo
pure $ P.fieldOptional fieldName fieldDesc fieldParser
`mapField` \value -> (pgiColumn columnInfo, updOpExp value)
let objName = tableName <> opName <> $$(G.litName "_input")
let objName = tableGQLName <> opName <> $$(G.litName "_input")
pure $ (G.unName opName,)
$ P.fieldOptional opName (Just opDesc)
$ P.object objName (Just objDesc)
@ -496,7 +495,7 @@ mutationSelectionSet
-> m (Parser 'Output n (RQL.MutFldsG 'Postgres UnpreparedValue))
mutationSelectionSet table selectPerms =
memoizeOn 'mutationSelectionSet table do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
returning <- runMaybeT do
permissions <- MaybeT $ pure selectPerms
tableSet <- lift $ tableSelectionList table permissions
@ -505,7 +504,7 @@ mutationSelectionSet table selectPerms =
pure $ RQL.MRet <$> P.subselection_ returningName (Just returningDesc) tableSet
let affectedRowsName = $$(G.litName "affected_rows")
affectedRowsDesc = "number of rows affected by the mutation"
selectionName = tableName <> $$(G.litName "_mutation_response")
selectionName = tableGQLName <> $$(G.litName "_mutation_response")
selectionDesc = G.Description $ "response of any mutation on the table " <>> table
selectionFields = catMaybes

View File

@ -37,7 +37,8 @@ orderByExp
-> SelPermInfo 'Postgres
-> m (Parser 'Input n [RQL.AnnOrderByItemG 'Postgres UnpreparedValue])
orderByExp table selectPermissions = memoizeOn 'orderByExp table $ do
name <- qualifiedObjectToName table <&> (<> $$(G.litName "_order_by"))
tableGQLName <- getTableGQLName table
let name = tableGQLName <> $$(G.litName "_order_by")
let description = G.Description $
"Ordering options when selecting data from " <> table <<> "."
tableFields <- tableSelectFields table selectPermissions
@ -90,8 +91,8 @@ orderByAggregation table selectPermissions = do
-- there is heavy duplication between this and Select.tableAggregationFields
-- it might be worth putting some of it in common, just to avoid issues when
-- we change one but not the other?
tableName <- qualifiedObjectToName table
allColumns <- tableSelectColumns table selectPermissions
tableGQLName <- getTableGQLName table
allColumns <- tableSelectColumns table selectPermissions
let numColumns = onlyNumCols allColumns
compColumns = onlyComparableCols allColumns
numFields = catMaybes <$> traverse mkField numColumns
@ -103,13 +104,13 @@ orderByAggregation table selectPermissions = do
, -- operators on numeric columns
if null numColumns then Nothing else Just $
for numericAggOperators \operator ->
parseOperator operator tableName numFields
parseOperator operator tableGQLName numFields
, -- operators on comparable columns
if null compColumns then Nothing else Just $
for comparisonAggOperators \operator ->
parseOperator operator tableName compFields
parseOperator operator tableGQLName compFields
]
let objectName = tableName <> $$(G.litName "_aggregate_order_by")
let objectName = tableGQLName <> $$(G.litName "_aggregate_order_by")
description = G.Description $ "order by aggregate values of table " <>> table
pure $ P.object objectName (Just description) aggFields
where
@ -123,15 +124,13 @@ orderByAggregation table selectPermissions = do
-> G.Name
-> InputFieldsParser n [(ColumnInfo 'Postgres, OrderInfo)]
-> InputFieldsParser n (Maybe [OrderByItemG (RQL.AnnAggregateOrderBy 'Postgres)])
parseOperator operator tableName columns =
parseOperator operator tableGQLName columns =
let opText = G.unName operator
objectName = tableName <> $$(G.litName "_") <> operator <> $$(G.litName "_order_by")
objectName = tableGQLName <> $$(G.litName "_") <> operator <> $$(G.litName "_order_by")
objectDesc = Just $ G.Description $ "order by " <> opText <> "() on columns of table " <>> table
in P.fieldOptional operator Nothing (P.object objectName objectDesc columns)
`mapField` map (\(col, info) -> mkOrderByItemG (RQL.AAOOp opText col) info)
orderByOperator :: MonadParse m => Parser 'Both m (Maybe OrderInfo)
orderByOperator =
P.nullable $ P.enum $$(G.litName "order_by") (Just "column ordering options") $ NE.fromList

View File

@ -207,11 +207,11 @@ selectTableAggregate
selectTableAggregate table fieldName description selectPermissions = runMaybeT do
guard $ spiAllowAgg selectPermissions
stringifyNum <- asks $ qcStringifyNum . getter
tableName <- lift $ qualifiedObjectToName table
tableGQLName <- lift $ getTableGQLName table
tableArgsParser <- lift $ tableArgs table selectPermissions
aggregateParser <- lift $ tableAggregationFields table selectPermissions
nodesParser <- lift $ tableSelectionList table selectPermissions
let selectionName = tableName <> $$(G.litName "_aggregate")
let selectionName = tableGQLName <> $$(G.litName "_aggregate")
aggregationParser = P.nonNullableParser $
parsedSelectionsToFields RQL.TAFExp <$>
P.selectionSet selectionName (Just $ G.Description $ "aggregated selection of " <>> table)
@ -300,7 +300,7 @@ tableSelectionSet
-> m (Parser 'Output n (AnnotatedFields 'Postgres))
tableSelectionSet table selectPermissions = memoizeOn 'tableSelectionSet table do
tableInfo <- _tiCoreInfo <$> askTableInfo table
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
let tableFields = Map.elems $ _tciFieldInfoMap tableInfo
tablePkeyColumns = _pkColumns <$> _tciPrimaryKey tableInfo
description = Just $ mkDescriptionWith (_tciDescription tableInfo) $
@ -324,10 +324,10 @@ tableSelectionSet table selectPermissions = memoizeOn 'tableSelectionSet table d
P.selection_ $$(G.litName "id") Nothing P.identifier $> RQL.AFNodeId table pkeyColumns
allFieldParsers = fieldParsers <> [nodeIdFieldParser]
nodeInterface <- node
pure $ P.selectionSetObject tableName description allFieldParsers [nodeInterface]
pure $ P.selectionSetObject tableGQLName description allFieldParsers [nodeInterface]
<&> parsedSelectionsToFields RQL.AFExpression
_ ->
pure $ P.selectionSetObject tableName description fieldParsers []
pure $ P.selectionSetObject tableGQLName description fieldParsers []
<&> parsedSelectionsToFields RQL.AFExpression
-- | List of table fields object.
@ -378,9 +378,9 @@ tableConnectionSelectionSet
-> SelPermInfo 'Postgres
-> m (Parser 'Output n (RQL.ConnectionFields 'Postgres UnpreparedValue))
tableConnectionSelectionSet table selectPermissions = do
tableName <- qualifiedObjectToName table
edgesParser <- tableEdgesSelectionSet
let connectionTypeName = tableName <> $$(G.litName "Connection")
edgesParser <- tableEdgesSelectionSet
tableGQLName <- getTableGQLName table
let connectionTypeName = tableGQLName <> $$(G.litName "Connection")
pageInfo = P.subselection_ $$(G.litName "pageInfo") Nothing
pageInfoSelectionSet <&> RQL.ConnectionPageInfo
edges = P.subselection_ $$(G.litName "edges") Nothing
@ -407,12 +407,13 @@ tableConnectionSelectionSet table selectPermissions = do
in P.nonNullableParser $ P.selectionSet $$(G.litName "PageInfo") Nothing allFields
<&> parsedSelectionsToFields RQL.PageInfoTypename
tableEdgesSelectionSet
:: m (Parser 'Output n (RQL.EdgeFields 'Postgres UnpreparedValue))
tableEdgesSelectionSet = do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
edgeNodeParser <- P.nonNullableParser <$> tableSelectionSet table selectPermissions
let edgesType = tableName <> $$(G.litName "Edge")
let edgesType = tableGQLName <> $$(G.litName "Edge")
cursor = P.selection_ $$(G.litName "cursor") Nothing
P.string $> RQL.EdgeCursor
edgeNode = P.subselection_ $$(G.litName "node") Nothing
@ -455,10 +456,11 @@ selectFunctionAggregate function fieldName description selectPermissions = runMa
let table = fiReturnType function
stringifyNum <- asks $ qcStringifyNum . getter
guard $ spiAllowAgg selectPermissions
tableGQLName <- getTableGQLName table
tableArgsParser <- lift $ tableArgs table selectPermissions
functionArgsParser <- lift $ customSQLFunctionArgs function
aggregateParser <- lift $ tableAggregationFields table selectPermissions
selectionName <- lift $ qualifiedObjectToName table <&> (<> $$(G.litName "_aggregate"))
selectionName <- lift $ pure tableGQLName <&> (<> $$(G.litName "_aggregate"))
nodesParser <- lift $ tableSelectionList table selectPermissions
let argsParser = liftA2 (,) functionArgsParser tableArgsParser
aggregationParser = fmap (parsedSelectionsToFields RQL.TAFExp) $
@ -785,11 +787,11 @@ tableAggregationFields
-> SelPermInfo 'Postgres
-> m (Parser 'Output n (RQL.AggregateFields 'Postgres))
tableAggregationFields table selectPermissions = do
tableName <- qualifiedObjectToName table
tableGQLName <- getTableGQLName table
allColumns <- tableSelectColumns table selectPermissions
let numericColumns = onlyNumCols allColumns
comparableColumns = onlyComparableCols allColumns
selectName = tableName <> $$(G.litName "_aggregate_fields")
selectName = tableGQLName <> $$(G.litName "_aggregate_fields")
description = G.Description $ "aggregate fields of " <>> table
count <- countField
numericAndComparable <- fmap concat $ sequenceA $ catMaybes
@ -797,12 +799,12 @@ tableAggregationFields table selectPermissions = do
if null numericColumns then Nothing else Just $
for numericAggOperators $ \operator -> do
numFields <- mkNumericAggFields operator numericColumns
pure $ parseAggOperator operator tableName numFields
pure $ parseAggOperator operator tableGQLName numFields
, -- operators on comparable columns
if null comparableColumns then Nothing else Just $ do
comparableFields <- traverse mkColumnAggField comparableColumns
pure $ comparisonAggOperators & map \operator ->
parseAggOperator operator tableName comparableFields
parseAggOperator operator tableGQLName comparableFields
]
let aggregateFields = count : numericAndComparable
pure $ P.selectionSet selectName (Just description) aggregateFields
@ -841,9 +843,9 @@ tableAggregationFields table selectPermissions = do
-> G.Name
-> [FieldParser n (RQL.ColFld 'Postgres)]
-> FieldParser n (RQL.AggregateField 'Postgres)
parseAggOperator operator tableName columns =
parseAggOperator operator tableGQLName columns =
let opText = G.unName operator
setName = tableName <> $$(G.litName "_") <> operator <> $$(G.litName "_fields")
setName = tableGQLName <> $$(G.litName "_") <> operator <> $$(G.litName "_fields")
setDesc = Just $ G.Description $ "aggregate " <> opText <> " on columns"
subselectionParser = P.selectionSet setName setDesc columns
<&> parsedSelectionsToFields RQL.CFExp

View File

@ -42,9 +42,9 @@ tableSelectColumnsEnum
-> SelPermInfo 'Postgres
-> m (Maybe (Parser 'Both n PGCol))
tableSelectColumnsEnum table selectPermissions = do
tableName <- qualifiedObjectToName table
columns <- tableSelectColumns table selectPermissions
let enumName = tableName <> $$(G.litName "_select_column")
tableGQLName <- getTableGQLName table
columns <- tableSelectColumns table selectPermissions
let enumName = tableGQLName <> $$(G.litName "_select_column")
description = Just $ G.Description $
"select columns of table " <>> table
pure $ P.enum enumName description <$> nonEmpty
@ -71,9 +71,9 @@ tableUpdateColumnsEnum
-> UpdPermInfo 'Postgres
-> m (Maybe (Parser 'Both n PGCol))
tableUpdateColumnsEnum table updatePermissions = do
tableName <- qualifiedObjectToName table
columns <- tableUpdateColumns table updatePermissions
let enumName = tableName <> $$(G.litName "_update_column")
tableGQLName <- getTableGQLName table
columns <- tableUpdateColumns table updatePermissions
let enumName = tableGQLName <> $$(G.litName "_update_column")
description = Just $ G.Description $
"update columns of table " <>> table
pure $ P.enum enumName description <$> nonEmpty

View File

@ -476,12 +476,12 @@ updateColMap fromQT toQT rnCol =
possiblyUpdateCustomColumnNames
:: MonadTx m => QualifiedTable -> PGCol -> PGCol -> m ()
possiblyUpdateCustomColumnNames qt oCol nCol = do
TableConfig customRootFields customColumns <- getTableConfig qt
TableConfig customRootFields customColumns identifier <- getTableConfig qt
let updatedCustomColumns =
M.fromList $ flip map (M.toList customColumns) $
\(dbCol, val) -> (, val) $ if dbCol == oCol then nCol else dbCol
when (updatedCustomColumns /= customColumns) $
updateTableConfig qt $ TableConfig customRootFields updatedCustomColumns
updateTableConfig qt $ TableConfig customRootFields updatedCustomColumns identifier
-- database functions for relationships
getRelDef :: QualifiedTable -> RelName -> Q.TxE QErr Value

View File

@ -17,6 +17,9 @@ module Hasura.RQL.DDL.Schema.Table
, SetTableCustomFields(..)
, runSetTableCustomFieldsQV2
, SetTableCustomization(..)
, runSetTableCustomization
, buildTableCache
, delTableAndDirectDeps
, processTableChanges
@ -196,6 +199,13 @@ runSetExistingTableIsEnumQ (SetTableIsEnum tableName isEnum) = do
buildSchemaCacheFor (MOTable tableName)
return successMsg
data SetTableCustomization
= SetTableCustomization
{ _stcTable :: !QualifiedTable
, _stcConfiguration :: !TableConfig
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 4 snakeCase) ''SetTableCustomization)
data SetTableCustomFields
= SetTableCustomFields
{ _stcfTable :: !QualifiedTable
@ -215,10 +225,19 @@ runSetTableCustomFieldsQV2
:: (MonadTx m, CacheRWM m) => SetTableCustomFields -> m EncJSON
runSetTableCustomFieldsQV2 (SetTableCustomFields tableName rootFields columnNames) = do
void $ askTabInfo tableName -- assert that table is tracked
updateTableConfig tableName (TableConfig rootFields columnNames)
-- `Identifier` is set to `Nothing` below because this API doesn't accept it
updateTableConfig tableName (TableConfig rootFields columnNames Nothing)
buildSchemaCacheFor (MOTable tableName)
return successMsg
runSetTableCustomization
:: (MonadTx m, CacheRWM m) => SetTableCustomization -> m EncJSON
runSetTableCustomization (SetTableCustomization table config) = do
void $ askTabInfo table
updateTableConfig table config
buildSchemaCacheFor (MOTable table)
return successMsg
unTrackExistingTableOrViewP1
:: (CacheRM m, QErrM m) => UntrackTable -> m ()
unTrackExistingTableOrViewP1 (UntrackTable vn _) = do
@ -271,7 +290,8 @@ processTableChanges ti tableDiff = do
procAlteredCols sc tn
withNewTabName newTN = do
let tnGQL = snakeCaseQualifiedObject newTN
let customTableNameText = G.unName <$> (_tcCustomName . _tciCustomConfig $ ti)
tnGQL = fromMaybe (snakeCaseQualifiedObject newTN) customTableNameText
-- check for GraphQL schema conflicts on new name
checkConflictingNode sc tnGQL
procAlteredCols sc tn
@ -287,10 +307,10 @@ processTableChanges ti tableDiff = do
TableDiff mNewName droppedCols _ alteredCols _ computedFieldDiff _ _ = tableDiff
possiblyDropCustomColumnNames tn = do
let TableConfig customFields customColumnNames = _tciCustomConfig ti
let TableConfig customFields customColumnNames identifier = _tciCustomConfig ti
modifiedCustomColumnNames = foldl' (flip Map.delete) customColumnNames droppedCols
when (modifiedCustomColumnNames /= customColumnNames) $
liftTx $ updateTableConfig tn $ TableConfig customFields modifiedCustomColumnNames
liftTx $ updateTableConfig tn $ TableConfig customFields modifiedCustomColumnNames identifier
procAlteredCols sc tn = for_ alteredCols $
\( RawColumnInfo oldName _ oldType _ _

View File

@ -392,20 +392,22 @@ data TableConfig
= TableConfig
{ _tcCustomRootFields :: !TableCustomRootFields
, _tcCustomColumnNames :: !CustomColumnNames
, _tcCustomName :: !(Maybe G.Name)
} deriving (Show, Eq, Lift, Generic)
instance NFData TableConfig
instance Cacheable TableConfig
$(deriveToJSON (aesonDrop 3 snakeCase) ''TableConfig)
$(deriveToJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''TableConfig)
emptyTableConfig :: TableConfig
emptyTableConfig =
TableConfig emptyCustomRootFields M.empty
TableConfig emptyCustomRootFields M.empty Nothing
instance FromJSON TableConfig where
parseJSON = withObject "TableConfig" $ \obj ->
TableConfig
<$> obj .:? "custom_root_fields" .!= emptyCustomRootFields
<*> obj .:? "custom_column_names" .!= M.empty
<*> obj .:? "custom_name"
-- | The @field@ and @primaryKeyColumn@ type parameters vary as the schema cache is built and more
-- information is accumulated. See 'TableRawInfo' and 'TableCoreInfo'.
@ -435,7 +437,8 @@ type TableCoreInfo b = TableCoreInfoG (FieldInfo b) (ColumnInfo b)
tciUniqueOrPrimaryKeyConstraints :: TableCoreInfoG a b -> Maybe (NonEmpty Constraint)
tciUniqueOrPrimaryKeyConstraints info = NE.nonEmpty $
maybeToList (_pkConstraint <$> _tciPrimaryKey info) <> toList (_tciUniqueConstraints info)
maybeToList (_pkConstraint <$> _tciPrimaryKey info)
<> toList (_tciUniqueConstraints info)
data TableInfo (b :: Backend)
= TableInfo

View File

@ -129,11 +129,12 @@ data RQLQueryV1
| RQDumpInternalState !DumpInternalState
| RQSetCustomTypes !CustomTypes
| RQSetTableCustomization !SetTableCustomization
deriving (Show, Eq)
data RQLQueryV2
= RQV2TrackTable !TrackTableV2
| RQV2SetTableCustomFields !SetTableCustomFields
| RQV2SetTableCustomFields !SetTableCustomFields -- deprecated
| RQV2TrackFunction !TrackFunctionV2
deriving (Show, Eq)
@ -304,6 +305,7 @@ queryModifiesSchemaCache (RQV1 qi) = case qi of
RQDumpInternalState _ -> False
RQSetCustomTypes _ -> True
RQSetTableCustomization _ -> True
RQBulk qs -> any queryModifiesSchemaCache qs
@ -440,6 +442,7 @@ runQueryM env rq = withPathK "args" $ case rq of
RQRunSql q -> runRunSQL q
RQSetCustomTypes q -> runSetCustomTypes q
RQSetTableCustomization q -> runSetTableCustomization q
RQBulk qs -> encJFromList <$> indexedMapM (runQueryM env) qs
@ -528,6 +531,7 @@ requiresAdmin = \case
RQDumpInternalState _ -> True
RQSetCustomTypes _ -> True
RQSetTableCustomization _ -> True
RQRunSql _ -> True

View File

@ -0,0 +1,104 @@
description: GraphQL introspection query
url: /v1/graphql
status: 200
query:
query: |
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,20 @@
type: bulk
args:
- type: run_sql
args:
sql: |
CREATE TABLE "user address" (
id serial primary key,
name text unique,
config jsonb
);
ALTER INDEX "user address_pkey" RENAME TO user_address_pkey;
ALTER INDEX "user address_name_key" RENAME TO user_address_name_key;
- type: track_table
version: 2
args:
table: user address
configuration:
custom_name: user_address

View File

@ -0,0 +1,4 @@
type: run_sql
args:
sql: |
DROP TABLE "user address" cascade;

View File

@ -0,0 +1,34 @@
description: deletes from table "author details" with custom name "author_details"
status: 200
url: /v1/graphql
response:
data:
delete_author_details:
affected_rows: 1
returning:
- id: 1
name: Author 1
delete_author_details_by_pk:
id: 2
name: Author 2
query:
query: |
mutation {
delete_author_details(
where: {
id: {_eq: 1}
}
) {
affected_rows
returning {
id
name
}
}
delete_author_details_by_pk(
id: 2
) {
id
name
}
}

View File

@ -0,0 +1,41 @@
description: Insert into article table with a new author
url: /v1/graphql
status: 200
response:
data:
insert_article:
affected_rows: 2
returning:
- title: Article 4 by new author
content: Content for Article 4
author:
id: 3
name: Author 3
query:
query: |
mutation {
insert_article(
objects: [
{
title: "Article 4 by new author",
content: "Content for Article 4"
author: {
data: {
id: 3
name: "Author 3"
}
}
}
]
){
affected_rows
returning{
title
content
author{
id
name
}
}
}
}

View File

@ -0,0 +1,37 @@
description: Insert into "author details" table
url: /v1/graphql
status: 200
response:
data:
insert_author_details:
affected_rows: 1
returning:
- id: 3
name: Author 3
phone: "1234567890"
insert_author_details_one:
id: 3
name: Author 3 updated
query:
query: |
mutation {
insert_author_details(
objects: [{name: "Author 3", phone: "1234567890"}]
){
affected_rows
returning{
id
name
phone
}
}
insert_author_details_one(
object: {name: "Author 3 updated", phone:"1234567890"},
on_conflict: { constraint: author_details_phone_key, update_columns: [name]}
){
id
name
}
}

View File

@ -0,0 +1,48 @@
type: bulk
args:
- type: run_sql
args:
sql: |
-- create tables
CREATE TABLE "author details" (
id SERIAL PRIMARY KEY,
name TEXT,
phone TEXT UNIQUE
);
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
author_id INTEGER REFERENCES "author details"(id) ON DELETE CASCADE
);
ALTER INDEX "author details_pkey" RENAME TO author_details_pkey;
ALTER INDEX "author details_phone_key" RENAME TO author_details_phone_key;
- type: track_table
version: 2
args:
table: author details
configuration:
custom_name: author_details
- type: track_table
version: 2
args:
table: article
configuration: {}
- type: create_array_relationship
args:
table: author details
name: articles
using:
foreign_key_constraint_on:
table: article
column: author_id
- type: create_object_relationship
args:
table: article
name: author
using:
foreign_key_constraint_on: author_id

View File

@ -0,0 +1,8 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DROP TABLE article;
DROP TABLE "author details";
cascade: true

View File

@ -0,0 +1,42 @@
description: updates in table "author details" with custom name "author_details"
status: 200
url: /v1/graphql
response:
data:
update_author_details:
affected_rows: 1
returning:
- id: 1
name: Author 1 Updated
update_author_details_by_pk:
id: 1
name: Author 1 Updated Again
query:
query: |
mutation {
update_author_details(
_set: {
name: "Author 1 Updated"
},
where: {
id: {_eq: 1}
}
) {
affected_rows
returning {
id
name
}
}
update_author_details_by_pk(
_set: {
name: "Author 1 Updated Again"
},
pk_columns: {
id: 1
}
) {
id
name
}
}

View File

@ -0,0 +1,24 @@
type: bulk
args:
- type: run_sql
args:
sql: |
-- insert data
INSERT INTO "author details" (name)
VALUES ('Author 1'), ('Author 2')
;
INSERT INTO article (title, content, author_id)
VALUES
( 'Article 1'
, 'Content for Article 1'
, 1
),
( 'Article 2'
, 'Content for Article 2'
, 1
),
( 'Article 3'
, 'Content for Article 3'
, 2
)
;

View File

@ -0,0 +1,9 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DELETE FROM article;
SELECT setval('article_id_seq', 1, FALSE);
DELETE FROM "author details";
SELECT setval('"author details_id_seq"', 1, FALSE);

View File

@ -0,0 +1,72 @@
description: Query author table with customised root field names
url: /v1/graphql
status: 200
response:
data:
author_details_by_pk:
id: 1
name: Author 1
author_details:
- id: 1
name: Author 1
articles:
- id: 1
title: Article 1
content: Content for Article 1
author_id: 1
- id: 2
title: Article 2
content: Content for Article 2
author_id: 1
- id: 2
name: Author 2
articles:
- id: 3
title: Article 3
content: Content for Article 3
author_id: 2
author_details_with_bool_exp:
- id: 2
name: Author 2
author_details_aggregate:
aggregate:
count: 2
nodes:
- id: 1
name: Author 1
- id: 2
name: Author 2
query:
query: |
query {
author_details_by_pk(id: 1){
id
name
}
author_details{
id
name
articles {
id
title
content
author_id
}
}
author_details_with_bool_exp: author_details(where: {id: {_eq: 2}}){
id
name
}
author_details_aggregate{
aggregate{
count
}
nodes{
id
name
}
}
}

View File

@ -0,0 +1,62 @@
type: bulk
args:
- type: run_sql
args:
sql: |
-- create tables
CREATE TABLE "author details" (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE
);
ALTER INDEX "author details_pkey" RENAME TO author_details_pkey;
ALTER INDEX "author details_name_key" RENAME TO author_details_name_key;
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
author_id INTEGER REFERENCES "author details"(id)
);
-- insert data
INSERT INTO "author details" (name)
VALUES ('Author 1'), ('Author 2')
;
INSERT INTO article (title, content, author_id)
VALUES
( 'Article 1'
, 'Content for Article 1'
, 1
),
( 'Article 2'
, 'Content for Article 2'
, 1
),
( 'Article 3'
, 'Content for Article 3'
, 2
)
;
- type: track_table
version: 2
args:
table: author details
configuration:
custom_name: author_details
- type: track_table
version: 2
args:
table: article
configuration: {}
- type: create_array_relationship
args:
table: author details
name: articles
using:
foreign_key_constraint_on:
table: article
column: author_id

View File

@ -0,0 +1,8 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DROP TABLE article;
DROP TABLE "author details";
cascade: true

View File

@ -112,6 +112,14 @@ response:
name: test2
column_mapping:
id: id
- table:
schema: public
name: user address
configuration:
custom_name: user_address
custom_root_fields: {}
custom_column_names: {}
functions:
- function:
schema: public

View File

@ -191,6 +191,24 @@ args:
- type: add_collection_to_allowlist
args:
collection: collection_1
- type: add_collection_to_allowlist
args:
collection: collection_2
- type: run_sql
args:
sql: |
CREATE TABLE "user address" (
id serial primary key,
name text,
address text
);
ALTER INDEX "user address_pkey" RENAME TO user_address_pkey;
- type: track_table
version: 2
args:
table: user address
configuration:
custom_name: user_address

View File

@ -5,6 +5,7 @@ args:
sql: |
DROP TABLE test1 cascade;
DROP TABLE test2 cascade;
DROP TABLE "user address" cascade;
cascade: true
- type: clear_metadata
args: {}

View File

@ -0,0 +1,47 @@
- description: Set custom column names
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: author
configuration:
custom_root_fields:
select: Authors
custom_column_names:
id: AuthorId
name: AuthorName
- description: "Rename column 'id' and drop column 'name'"
url: /v1/query
status: 200
response:
result_type: CommandOk
result: null
query:
type: run_sql
args:
sql: |
ALTER TABLE author DROP COLUMN name;
ALTER TABLE author RENAME COLUMN id to author_id;
- description: Test if custom column names are updated
url: /v1/graphql
status: 200
response:
data:
Authors:
- AuthorId: 1
age: 23
- AuthorId: 2
age: null
query:
query: |
query {
Authors{
AuthorId
age
}
}

View File

@ -0,0 +1,71 @@
- description: Set custom column names for article table by swaping the column names
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: article
configuration:
custom_root_fields: {}
custom_column_names:
title: content
content: title
- description: Perform graphql query
url: /v1/graphql
status: 200
response:
data:
article:
- id: 1
title: Article 1 content
content: Article 1 title
- id: 2
title: Article 2 content
content: Article 2 title
query:
query: |
query {
article{
id
title
content
}
}
- description: Unset the custom column names
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: article
configuration:
custom_root_fields: {}
custom_column_names: {}
- description: Peform graphql query
url: /v1/graphql
status: 200
response:
data:
article:
- id: 1
title: Article 1 title
content: Article 1 content
- id: 2
title: Article 2 title
content: Article 2 content
query:
query: |
query {
article{
id
title
content
}
}

View File

@ -0,0 +1,32 @@
description: Set custom column names conflicting with existing relationship
url: /v1/query
status: 400
response:
internal:
- definition:
using:
foreign_key_constraint_on:
column: author_id
table:
schema: public
name: article
name: articles
comment:
table:
schema: public
name: author
reason: field definition conflicts with custom field name for postgres column
"name"
type: array_relation
path: $.args
error: cannot continue due to new inconsistent metadata
code: unexpected
query:
type: set_table_customization
args:
table: author
configuration:
custom_root_fields: {}
custom_column_names:
name: articles
age: articles_aggregate

View File

@ -0,0 +1,13 @@
description: set custom table name which conflicts with other nodes
status: 500
url: /v1/query
response:
error: "found duplicate fields in selection set: insert_article, insert_article_one, update_article, delete_article, update_article_by_pk, delete_article_by_pk"
code: unexpected
path: $.args
query:
type: set_table_customization
args:
table: author
configuration:
custom_name: article # article table has already been tracked

View File

@ -0,0 +1,32 @@
description: Try to define a relationship with custom column name
url: /v1/query
status: 400
response:
internal:
- definition:
using:
foreign_key_constraint_on:
column: author_id
table:
schema: public
name: article
name: AuthorId
comment:
table:
schema: public
name: author
reason: field definition conflicts with custom field name for postgres column
"id"
type: array_relation
path: $.args
error: field definition conflicts with custom field name for postgres column "id"
code: constraint-violation
query:
type: create_array_relationship
args:
name: AuthorId
table: author
using:
foreign_key_constraint_on:
table: article
column: author_id

View File

@ -0,0 +1,55 @@
- description: set table customization with custom name
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: author
configuration:
custom_name: author_alias
custom_root_fields: {}
custom_column_names: {}
- description: Check that above query has changed the schema
url: /v1/graphql
status: 200
response:
data:
author_alias_by_pk:
name: Clarke
query:
query: |
query {
author_alias_by_pk(id: 1){
name
}
}
- description: rename the underlying table
url: /v1/query
status: 200
response:
result_type: CommandOk
result: null
query:
type: run_sql
args:
sql: |
ALTER TABLE author RENAME TO authors;
- description: Check if the earlier query works after renaming the table
url: /v1/graphql
status: 200
response:
data:
author_alias_by_pk:
name: Clarke
query:
query: |
query {
author_alias_by_pk(id: 1){
name
}
}

View File

@ -0,0 +1,55 @@
- description: set table configuration with custom name
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_configuration
args:
table: author
configuration:
custom_name: author_alias
custom_root_fields: {}
custom_column_names: {}
- description: Check that above query has changed the schema
url: /v1/graphql
status: 200
response:
data:
author_alias_by_pk:
name: Clarke
query:
query: |
query {
author_alias_by_pk(id: 1){
name
}
}
- description: rename the underlying table
url: /v1/query
status: 200
response:
result_type: CommandOk
result: null
query:
type: run_sql
args:
sql: |
ALTER TABLE author RENAME TO authors;
- description: Check if the earlier query works after renaming the table
url: /v1/graphql
status: 200
response:
data:
author_alias_by_pk:
name: Clarke
query:
query: |
query {
author_alias_by_pk(id: 1){
name
}
}

View File

@ -0,0 +1,60 @@
- description: Set select_by_pk root field and customise name column
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: author
configuration:
custom_root_fields:
select: Authors
select_by_pk: Author
custom_column_names:
id: AuthorId
name: AuthorName
age: age
- description: Check that above query has changed the schema
url: /v1/graphql
status: 200
response:
data:
Author:
AuthorName: Clarke
query:
query: |
query {
Author(AuthorId: 1){
AuthorName
}
}
- description: Unset select_by_pk root field and remove custom column names
url: /v1/query
status: 200
response:
message: success
query:
type: set_table_customization
args:
table: author
configuration:
custom_root_fields:
select: Authors
- description: Check that above query has changed the schema
url: /v1/graphql
status: 200
response:
data:
author_by_pk:
name: Clarke
query:
query: |
query {
author_by_pk(id: 1){
name
}
}

View File

@ -0,0 +1,18 @@
description: Set custom fields of table which does not exist
url: /v1/query
status: 400
response:
path: "$.args"
error: table "author1" does not exist
code: not-exists
query:
type: set_table_customization
args:
table: author1
configuration:
custom_root_fields:
select: Authors
select_by_pk: Author
custom_column_names:
id: AuthorId
name: AuthorName

View File

@ -0,0 +1,51 @@
type: bulk
args:
- type: run_sql
args:
sql: |
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER
);
INSERT INTO author (name, age) VALUES
('Clarke', 23),
('Bellamy', NULL);
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
author_id INTEGER NOT NULL REFERENCES author(id)
);
INSERT INTO article (title, content, author_id) VALUES
('Article 1 title', 'Article 1 content', 1),
('Article 2 title', 'Article 2 content', 2);
- type: track_table
version: 2
args:
table: author
configuration:
custom_root_fields:
select: Authors
custom_column_names:
id: AuthorId
- type: track_table
version: 2
args:
table: article
configuration: {}
- type: create_array_relationship
args:
name: articles
table: author
using:
foreign_key_constraint_on:
table: article
column: author_id

View File

@ -0,0 +1,9 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DROP TABLE article;
DROP TABLE IF EXISTS author;
DROP TABLE IF EXISTS authors;
cascade: true

View File

@ -32,3 +32,69 @@ class TestGraphqlIntrospection:
@classmethod
def dir(cls):
return "queries/graphql_introspection"
def getTypeNameFromType(typeObject):
if typeObject['name'] != None:
return typeObject['name']
elif isinstance(typeObject['ofType'],dict):
return getTypeNameFromType(typeObject['ofType'])
else:
raise Exception("typeObject doesn't have name and ofType is not an object")
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestGraphqlIntrospectionWithCustomTableName:
# test to check some of the type names that are generated
# while tracking a table with a custom name
def test_introspection(self, hge_ctx):
with open(self.dir() + "/introspection.yaml") as c:
conf = yaml.safe_load(c)
resp, _ = check_query(hge_ctx, conf)
hasMultiSelect = False
hasAggregate = False
hasSelectByPk = False
hasQueryRoot = False
for t in resp['data']['__schema']['types']:
if t['name'] == 'query_root':
hasQueryRoot = True
for field in t['fields']:
if field['name'] == 'user_address':
hasMultiSelect = True
assert 'args' in field
for args in field['args']:
if args['name'] == 'distinct_on':
assert "user_address_select_column" == getTypeNameFromType(args['type'])
elif args['name'] == 'order_by':
assert "user_address_order_by" == getTypeNameFromType(args['type'])
elif args['name'] == 'where':
assert 'user_address_bool_exp' == getTypeNameFromType(args['type'])
elif field['name'] == 'user_address_aggregate':
hasAggregate = True
assert "user_address_aggregate" == getTypeNameFromType(field['type'])
elif field['name'] == 'user_address_by_pk':
assert "user_address" == getTypeNameFromType(field['type'])
hasSelectByPk = True
elif t['name'] == 'mutation_root':
for field in t['fields']:
if field['name'] == 'insert_user_address':
hasMultiInsert = True
assert "user_address_mutation_response" == getTypeNameFromType(field['type'])
for args in field['args']:
if args['name'] == 'object':
assert "user_address_insert_input" == getTypeNameFromType(args['type'])
elif field['name'] == 'update_user_address_by_pk':
hasUpdateByPk = True
assert "user_address" == getTypeNameFromType(field['type'])
for args in field['args']:
if args['name'] == 'object':
assert "user_address" == getTypeNameFromType(args['type'])
assert hasQueryRoot
assert hasMultiSelect
assert hasAggregate
assert hasSelectByPk
assert hasMultiInsert
assert hasUpdateByPk
@classmethod
def dir(cls):
return "queries/graphql_introspection/custom_table_name"

View File

@ -543,6 +543,26 @@ class TestGraphqlMutationCustomSchema:
def dir(cls):
return "queries/graphql_mutation/custom_schema"
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@use_mutation_fixtures
class TestGraphqlMutationCustomGraphQLTableName:
def test_insert_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/insert_author_details.yaml', transport)
def test_insert_article_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/insert_article_author.yaml', transport)
def test_update_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/update_author_details.yaml', transport)
def test_delete_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/delete_author_details.yaml', transport)
@classmethod
def dir(cls):
return "queries/graphql_mutation/custom_schema/custom_table_name"
@pytest.mark.parametrize('transport', ['http', 'websocket'])
@use_mutation_fixtures
class TestGraphQLMutateEnums:

View File

@ -544,7 +544,7 @@ class TestGraphQLQueryFunctions:
@pytest.mark.parametrize("transport", ['http', 'websocket'])
def test_query_get_test_session_id(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/query_get_test_session_id.yaml')
@pytest.mark.parametrize("transport", ['http', 'websocket'])
def test_query_search_author_mview(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/query_search_author_mview.yaml')
@ -567,6 +567,17 @@ class TestGraphQLQueryCustomSchema:
def dir(cls):
return 'queries/graphql_query/custom_schema'
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@usefixtures('per_class_tests_db_state')
class TestGraphQLQueryCustomTableName:
def test_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + 'author.yaml', transport)
@classmethod
def dir(cls):
return 'queries/graphql_query/custom_schema/custom_table_name/'
@pytest.mark.parametrize('transport', ['http', 'websocket'])
@usefixtures('per_class_tests_db_state')
class TestGraphQLQueryEnums:

View File

@ -764,6 +764,37 @@ class TestSetTableCustomFields:
def test_relationship_conflict_with_custom_column(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/relationship_conflict_with_custom_column.yaml")
@usefixtures('per_method_tests_db_state')
class TestSetTableCustomization:
@classmethod
def dir(cls):
return 'queries/v1/set_table_configuration'
def test_set_and_unset(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/set_and_unset.yaml')
def test_set_invalid_table(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/set_invalid_table.yaml')
def test_alter_column(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/alter_column.yaml')
def test_conflict_with_relationship(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/conflict_with_relationship.yaml')
def test_column_field_swap(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/column_field_swap.yaml")
def test_relationship_conflict_with_custom_column(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/relationship_conflict_with_custom_column.yaml")
def test_alter_table_name_with_custom_name(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/rename_original_table_with_custom_name.yaml")
def test_conflicting_custom_table_name(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/fail_conflicting_custom_table_name.yaml")
@usefixtures('per_method_tests_db_state')
class TestComputedFields:
@classmethod