Merge branch 'master' into init-current-directory

This commit is contained in:
Shraddha Agrawal 2020-04-28 02:27:23 +05:30 committed by GitHub
commit 1002d20a0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 719 additions and 50 deletions

View File

@ -2,6 +2,36 @@
## Next release ## Next release
### Allow access to session variables by computed fields (fix #3846)
Sometimes it is useful for computed fields to have access to the Hasura session variables directly. For example, suppose you want to fetch some articles but also get related user info, say `likedByMe`. Now, you can define a function like:
```
CREATE OR REPLACE FUNCTION article_liked(article_row article, hasura_session json)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1
FROM liked_article A
WHERE A.user_id = hasura_session ->> 'x-hasura-user-id' AND A.article_id = article_row.id
);
$$ LANGUAGE sql STABLE;
```
and make a query like:
```
query {
articles {
title
content
likedByMe
}
}
```
Support for this is now added through the `add_computed_field` API.
Read more about the session argument for computed fields in the [docs](https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/computed-field.html).
### Bug fixes and improvements ### Bug fixes and improvements
- cli: allow initialising project in current directory (fix #4560) #4566 - cli: allow initialising project in current directory (fix #4560) #4566
@ -249,6 +279,7 @@ A new CLI migrations image is introduced to account for the new migrations workf
(close #3969) (#4145) (close #3969) (#4145)
### Bug fixes and improvements ### Bug fixes and improvements
- server: improve performance of replace_metadata tracking many tables (fix #3802) - server: improve performance of replace_metadata tracking many tables (fix #3802)
- server: option to reload remote schemas in 'reload_metadata' API (fix #3792, #4117) - server: option to reload remote schemas in 'reload_metadata' API (fix #3792, #4117)
- server: fix various space leaks to avoid excessive memory consumption - server: fix various space leaks to avoid excessive memory consumption
@ -332,3 +363,4 @@ Read more about it in the [docs](https://hasura.io/docs/1.0/graphql/manual/auth/
- server: check expression in update permissions (close #384) (rfc #3750) (#3804) - server: check expression in update permissions (close #384) (rfc #3750) (#3804)
- console: show pre-release update notifications with opt out option (#3888) - console: show pre-release update notifications with opt out option (#3888)
- console: handle invalid keys in permission builder (close #3848) (#3863) - console: handle invalid keys in permission builder (close #3848) (#3863)
- docs: add page on data validation to docs (close #4085) (#4260)

View File

@ -105,6 +105,12 @@ ComputedFieldDefinition
- String - String
- Name of the argument which accepts a table row type. If omitted, the first - Name of the argument which accepts a table row type. If omitted, the first
argument is considered a table argument argument is considered a table argument
* - session_argument
- false
- String
- Name of the argument which accepts the Hasura session object as
a JSON/JSONB value. If omitted, the Hasura session object is
not passed to the function
.. _drop_computed_field: .. _drop_computed_field:

View File

@ -16,8 +16,8 @@ What are computed fields?
------------------------- -------------------------
Computed fields are virtual values or objects that are dynamically computed and can be queried along with a table's Computed fields are virtual values or objects that are dynamically computed and can be queried along with a table's
columns. Computed fields are computed when requested for via SQL functions using other columns of the table and other columns. Computed fields are computed when requested for via `custom SQL functions <https://www.postgresql.org/docs/current/sql-createfunction.html>`__
custom inputs if needed. using other columns of the table and other custom inputs if needed.
.. note:: .. note::
@ -195,6 +195,76 @@ Computed fields permissions
- For **table computed fields**, the permissions set on the return table are respected. - For **table computed fields**, the permissions set on the return table are respected.
Accessing Hasura session variables in computed fields
-----------------------------------------------------
It can be useful to have access to the session variable from the SQL function defining a computed field. For instance, suppose we want to record which users have liked which articles. We can do so using a table ``article_likes`` that specifies a many-to-many relationship between ``article`` and ``user``. In such a case it can be useful to know if the current user has liked a specific article, and this information can be exposed as a *Boolean* computed field on ``article``.
Use the :ref:`add_computed_field` API to add a function, and specify the name of the argument taking a session argument in ``session_argument``. The session argument is a JSON object where keys are session variable names (in lower case) and values are strings. Use the ``->>`` JSON operator to fetch the value of a session variable as shown in the following example.
.. code-block:: plpgsql
-- 'hasura_session' will be the session argument
CREATE OR REPLACE FUNCTION article_liked_by_user(article_row article, hasura_session json)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1
FROM article_likes A
WHERE A.user_id = hasura_session ->> 'x-hasura-user-id' AND A.article_id = article_row.id
);
$$ LANGUAGE sql STABLE;
.. code-block:: http
POST /v1/query HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type":"add_computed_field",
"args":{
"table":{
"name":"article",
"schema":"public"
},
"name":"liked_by_user",
"definition":{
"function":{
"name":"article_liked_by_user",
"schema":"public"
},
"table_argument":"article_row",
"session_argument":"hasura_session"
}
}
}
.. graphiql::
:view_only:
:query:
query {
article(where: {id: {_eq: 3}}) {
id
liked_by_user
}
}
:response:
{
"data": {
"article": [
{
"id": "3",
"liked_by_user": true
}
]
}
}
.. note::
The specified session argument is not included in the ``<function-name>_args`` input object in the GraphQL schema.
Computed fields vs. Postgres generated columns Computed fields vs. Postgres generated columns
---------------------------------------------- ----------------------------------------------

View File

@ -4,8 +4,8 @@
.. _custom_sql_functions: .. _custom_sql_functions:
Customise schema with SQL functions Extend schema with SQL functions
=================================== ================================
.. contents:: Table of contents .. contents:: Table of contents
:backlinks: none :backlinks: none
@ -18,15 +18,19 @@ What are custom SQL functions?
Custom SQL functions are `user-defined SQL functions <https://www.postgresql.org/docs/current/sql-createfunction.html>`_ Custom SQL functions are `user-defined SQL functions <https://www.postgresql.org/docs/current/sql-createfunction.html>`_
that can be used to either encapsulate some custom business logic or extend the built-in SQL functions and operators. that can be used to either encapsulate some custom business logic or extend the built-in SQL functions and operators.
Hasura GraphQL engine lets you expose certain types of custom functions over the GraphQL API to allow querying them Hasura GraphQL engine lets you expose certain types of custom functions as top level fields in the GraphQL API to allow
using both ``queries`` and ``subscriptions``. querying them using both ``queries`` and ``subscriptions``.
.. note::
Custom SQL functions can also be queried as :ref:`computed fields <computed_fields>` of tables.
.. _supported_sql_functions: .. _supported_sql_functions:
Supported SQL functions Supported SQL functions
----------------------- ***********************
Currently, only functions which satisfy the following constraints can be exposed over the GraphQL API Currently, only functions which satisfy the following constraints can be exposed as top level fields in the GraphQL API
(*terminology from* `Postgres docs <https://www.postgresql.org/docs/current/sql-createfunction.html>`__): (*terminology from* `Postgres docs <https://www.postgresql.org/docs/current/sql-createfunction.html>`__):
- **Function behaviour**: ONLY ``STABLE`` or ``IMMUTABLE`` - **Function behaviour**: ONLY ``STABLE`` or ``IMMUTABLE``

View File

@ -0,0 +1,359 @@
.. meta::
:description: Data validations in Hasura
:keywords: hasura, docs, schema, data validation
.. _data_validations:
Data validations
================
.. contents:: Table of contents
:backlinks: none
:depth: 2
:local:
Overview
--------
Many times, we need to perform validations of input data before inserting or
updating objects.
The best solution to implement a validation depends on the complexity of the
validation logic and the layer where you would like to add it.
- If you would like the validation logic to be a part of your database schema,
Postgres check constraints or triggers would be ideal solutions to add your
validation.
- If you would like the validation logic to be at the GraphQL API layer, Hasura
permissions can be used to add your validation.
- If the validation logic requires complex business logic and/or needs
information from external sources, you can use Hasura Actions to perform your
validation.
These solutions are explained in some more detail below.
Using Postgres check constraints
--------------------------------
If the validation logic can be expressed by using only static values and the
columns of the table, you can use `Postgres check constraints <https://www.postgresql.org/docs/current/ddl-constraints.html>`__.
**Example:** Check that the ``rating`` for an author is between 1 and 10 only.
Let's say we have a table:
.. code-block:: sql
author (id uuid, name text, rating integer)
Now, we can head to the ``Modify`` tab in the table page and add a check
constraint in the ``Check Constraints`` section:
.. thumbnail:: ../../../img/graphql/manual/schema/validation-add-check-constraint.png
:alt: Add check constraint
If someone now tries to add an author with a rating of ``11``, the following
error is thrown:
.. graphiql::
:view_only:
:query:
mutation {
insert_author(
objects: {
name: "Enid Blyton",
rating: 11
}) {
affected_rows
}
}
:response:
{
"errors": [
{
"message": "Check constraint violation. new row for relation \"author\" violates check constraint \"authors_rating_check\"",
"extensions": {
"path": "$.selectionSet.insert_author.args.objects",
"code": "permission-error"
}
}
]
}
Learn more about `Postgres check constraints <https://www.postgresql.org/docs/current/ddl-constraints.html>`__.
Using Postgres triggers
-----------------------
If the validation logic is more complex and requires the use of data from other tables
and/or functions, then you can use `Postgres triggers <https://www.postgresql.org/docs/current/sql-createtrigger.html>`__.
**Example:** Validate that an article's ``content`` does not exceed a certain number of words.
Suppose we have the following table:
.. code-block:: sql
article (id uuid, title text, content text)
Now, we can head to the ``Data -> SQL`` tab in the console and
create a `Postgres function <https://www.postgresql.org/docs/current/sql-createfunction.html>`__
that checks if an article's content exceeds a certain number of words,
and then add a `Postgres trigger <https://www.postgresql.org/docs/current/sql-createtrigger.html>`__
that will call this function every time before an article is inserted or updated.
.. code-block:: plpgsql
CREATE FUNCTION check_content_length()
RETURNS trigger AS $$
DECLARE content_length INTEGER;
BEGIN
-- split article content into words and get count
select array_length(regexp_split_to_array(NEW.content, '\s'),1) INTO content_length;
-- throw an error if article content is too long
IF content_length > 100 THEN
RAISE EXCEPTION 'Content can not have more than 100 words';
END IF;
-- return the article row if no error
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER check_content_length_trigger
BEFORE INSERT OR UPDATE ON "article"
FOR EACH ROW
EXECUTE PROCEDURE check_content_length();
Now, if we try to insert an article whose content has more than 100 words, we'll receive
the following error:
.. graphiql::
:view_only:
:query:
mutation {
insert_article(
objects: {
title: "lorem ipsum"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et nisl dolor. Nulla eleifend odio et velit aliquet, sed convallis quam bibendum. Cras consequat elit quis est vehicula, nec dignissim dolor cursus. Phasellus suscipit magna ac turpis pulvinar ultricies. Nulla sed lacus sed metus egestas scelerisque nec sed urna. Fusce lorem velit, efficitur sed luctus in, fringilla ac urna. Maecenas fermentum augue sit amet malesuada imperdiet. Suspendisse mattis dignissim quam, at tempor dui tincidunt sed. Maecenas placerat erat nec erat aliquet rutrum. Mauris congue velit nec ultrices dapibus. Duis aliquam, est ac ultricies viverra, ante augue dignissim massa, quis iaculis ex dui in ex. Curabitur pharetra neque ac nisl fringilla, vel pellentesque orci molestie.",
}
) {
affected_rows
}
}
:response:
{
"errors": [
{
"message": "postgres query error",
"extensions": {
"internal": {
"error": {
"exec_status": "FatalError",
"message": "Content can not have more than 100 words",
"status_code": "P0001",
},
},
"path": "$.selectionSet.insert_article.args.objects",
"code": "unexpected"
}
}
]
}
Learn more about `Postgres triggers <https://www.postgresql.org/docs/current/sql-createtrigger.html>`__.
Using Hasura permissions
------------------------
If the validation logic can be expressed **declaratively** using static values and
data from the database, then you can use :ref:`row level permissions <row-level-permissions>`
to perform the validations. (Read more about :ref:`Authorization <authorization>`).
**Example 1:** Validate that an ``article`` can be inserted only if ``title`` is not empty.
Suppose, we have a table:
.. code-block:: sql
article (id uuid, title text, content text, author_id uuid)
Now, we can create a role ``user`` and add the following rule:
.. thumbnail:: ../../../img/graphql/manual/schema/validation-not-empty.png
:alt: validation using permission: title cannot be empty
If we try to insert an article with ``title = ""``, we will get a ``permission-error``:
.. graphiql::
:view_only:
:query:
mutation {
insert_article(
objects: {
title: ""
content: "Lorem ipsum dolor sit amet",
}
) {
affected_rows
}
}
:response:
{
"errors": [
{
"message": "Check constraint violation. insert check constraint failed",
"extensions": {
"path": "$.selectionSet.insert_article.args.objects",
"code": "permission-error"
}
}
]
}
**Example 2:** Validate that an ``article`` can be inserted only if its ``author`` is active.
Suppose, we have 2 tables:
.. code-block:: sql
author (id uuid, name text, is_active boolean)
article (id uuid, author_id uuid, content text)
Also, suppose there is an :ref:`object relationship <graphql_relationships>` ``article.author`` defined as:
.. code-block:: sql
article.author_id -> author.id
Now, we can create a role ``user`` and add the following rule:
.. thumbnail:: ../../../img/graphql/manual/schema/validation-author-isactive.png
:alt: validation using permissions: author should be active
If we try to insert an article for an author for whom ``is_active = false``, we
will receive a ``permission-error`` :
.. graphiql::
:view_only:
:query:
mutation {
insert_article(
objects: {
title: "lorem ipsum"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
author_id: 2
}
) {
affected_rows
}
}
:response:
{
"errors": [
{
"message": "Check constraint violation. insert check constraint failed",
"extensions": {
"path": "$.selectionSet.insert_article.args.objects",
"code": "permission-error"
}
}
]
}
.. note::
Permissions are scoped to a user's role. So, if a validation check
needs to be global then you will have to define it for all roles which have
insert/update permissions.
A few features on the roadmap should simplify this experience in the future.
Using Hasura Actions
--------------------
If the validation requires complex custom business logic and/or needs information
from external sources, you can use :ref:`Actions <actions>` to perform your
validation.
**Example:** Check with an external service that an author's name is not black-listed
before inserting them.
Let's assume we have an external service that stores and manages black-listed authors.
Before inserting an author we need to check with this service if they are black-listed
or not.
The validation process looks as follows:
.. thumbnail:: ../../../img/graphql/manual/schema/actions-data-validation.png
:alt: validation using actions: article not blacklisted
:width: 60%
Actions allow us to define :ref:`custom types <custom_types>` in our GraphQL schema.
We create a new action called ``InsertAuthor`` that takes an ``author`` object with type ``AuthorInput`` as input and
returns an object of type ``AuthorOutput``:
.. thumbnail:: ../../../img/graphql/manual/schema/validation-actions-def.png
:alt: Create action
The business logic of an action - in our case the author validation - happens in the :ref:`action handler <action_handlers>`
which is an HTTP webhook which contains the code to call the external service.
The following is a sample code that could be added to the event handler to implement the data validation:
.. code-block:: javascript
function getBlacklistedAuthorsFromApi() {
// make external api call & return black-listed authors list
}
function insertAuthorViaHasura() {
// run insert_author mutation & return response
}
const blacklistedAuthors = getBlacklistedAuthorsFromApi();
if (blacklistedAuthors.includes(author.name)) {
return res.status(400).json({ message: "Author is blacklisted" });
} else {
const insertAuthorResponse = insertAuthorViaHasura();
return res.json(insertAuthorResponse);
}
When we now insert an author, our action handler will be called and it will check if the author is black-listed.
If it's not, the author will be inserted and the ``id`` will be returned. If the author is black-listed,
we get the following error message:
.. graphiql::
:view_only:
:query:
mutation insertArticle {
InsertAuthor(author: { name: "Thanos" }) {
id
}
}
:response:
{
"errors": [
{
"extensions": {
"path": "$",
"code": "unexpected"
},
"message": "Author is blacklisted"
}
]
}
.. note::
For actual examples of data validations with actions, refer to the `actions examples repo <https://github.com/hasura/hasura-actions-examples/tree/master/data-validations>`__.

View File

@ -30,11 +30,12 @@ Postgres constructs.
Basics <basics> Basics <basics>
Relationships <relationships/index> Relationships <relationships/index>
Customise with views <views> Extend with views <views>
Customise with SQL functions <custom-functions> Extend with SQL functions <custom-functions>
Default field values <default-values/index> Default field values <default-values/index>
Enum type fields <enums> enums
computed-fields computed-fields
custom-field-names custom-field-names
data-validations
Using an existing database <using-existing-database> Using an existing database <using-existing-database>
Export GraphQL schema <export-graphql-schema> Export GraphQL schema <export-graphql-schema>

View File

@ -17,6 +17,8 @@ be connected via relationships.
Let's say we have the following tables in our database: ``author``, ``passport_info``, ``article`` and ``tag``. Let's say we have the following tables in our database: ``author``, ``passport_info``, ``article`` and ``tag``.
.. _table_relationships:
Table relationships Table relationships
------------------- -------------------
@ -36,11 +38,13 @@ following types of table relationships:
| | | - a ``tag`` can have many ``articles`` | | | | - a ``tag`` can have many ``articles`` |
+------------------+-----------------------------------+------------------------------------------------+ +------------------+-----------------------------------+------------------------------------------------+
.. _graphql_relationships:
GraphQL schema relationships GraphQL schema relationships
---------------------------- ----------------------------
As you can see, each table relationship will have two component relationships (one in either direction) in the GraphQL Each table relationship, as you can see from the above section, will have two component relationships
schema. These relationships can be one of the following types: (one in either direction) in the GraphQL schema. These relationships can be one of the following types:
+-----------------------------------------+------------------------------------------+---------------------------------------------------------------------------------------+ +-----------------------------------------+------------------------------------------+---------------------------------------------------------------------------------------+
| Type | Example | Meaning | | Type | Example | Meaning |

View File

@ -4,8 +4,8 @@
.. _custom_views: .. _custom_views:
Customise schema with views Extend schema with views
=========================== ========================
.. contents:: Table of contents .. contents:: Table of contents
:backlinks: none :backlinks: none

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -62,7 +62,7 @@ resolveComputedField
=> ComputedField -> Field -> m (RS.ComputedFieldSel UnresolvedVal) => ComputedField -> Field -> m (RS.ComputedFieldSel UnresolvedVal)
resolveComputedField computedField fld = fieldAsPath fld $ do resolveComputedField computedField fld = fieldAsPath fld $ do
funcArgs <- parseFunctionArgs argSeq argFn $ Map.lookup "args" $ _fArguments fld funcArgs <- parseFunctionArgs argSeq argFn $ Map.lookup "args" $ _fArguments fld
let argsWithTableArgument = withTableArgument funcArgs let argsWithTableArgument = withTableAndSessionArgument funcArgs
case fieldType of case fieldType of
CFTScalar scalarTy -> do CFTScalar scalarTy -> do
colOpM <- argsToColOp $ _fArguments fld colOpM <- argsToColOp $ _fArguments fld
@ -73,16 +73,25 @@ resolveComputedField computedField fld = fieldAsPath fld $ do
RS.CFSTable RS.JASMultipleRows <$> fromField functionFrom cols permFilter permLimit fld RS.CFSTable RS.JASMultipleRows <$> fromField functionFrom cols permFilter permLimit fld
where where
ComputedField _ function argSeq fieldType = computedField ComputedField _ function argSeq fieldType = computedField
ComputedFieldFunction qf _ tableArg _ = function ComputedFieldFunction qf _ tableArg sessionArg _ = function
argFn :: FunctionArgItem -> InputFunctionArgument
argFn = IFAUnknown argFn = IFAUnknown
withTableArgument resolvedArgs = withTableAndSessionArgument :: RS.FunctionArgsExpG UnresolvedVal
-> RS.FunctionArgsExpTableRow UnresolvedVal
withTableAndSessionArgument resolvedArgs =
let argsExp@(RS.FunctionArgsExp positional named) = RS.AEInput <$> resolvedArgs let argsExp@(RS.FunctionArgsExp positional named) = RS.AEInput <$> resolvedArgs
tableRowArg = RS.AETableRow Nothing tableRowArg = RS.AETableRow Nothing
in case tableArg of withTable = case tableArg of
FTAFirst -> FTAFirst ->
RS.FunctionArgsExp (tableRowArg:positional) named RS.FunctionArgsExp (tableRowArg:positional) named
FTANamed argName index -> FTANamed argName index ->
RS.insertFunctionArg argName index tableRowArg argsExp RS.insertFunctionArg argName index tableRowArg argsExp
sessionArgVal = RS.AESession UVSession
alsoWithSession = case sessionArg of
Nothing -> withTable
Just (FunctionSessionArgument argName index) ->
RS.insertFunctionArg argName index sessionArgVal withTable
in alsoWithSession
processTableSelectionSet processTableSelectionSet
:: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r

View File

@ -315,7 +315,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi
-- computed fields' function args input objects and scalar types -- computed fields' function args input objects and scalar types
mkComputedFieldRequiredTypes computedFieldInfo = mkComputedFieldRequiredTypes computedFieldInfo =
let ComputedFieldFunction qf inputArgs _ _ = _cfFunction computedFieldInfo let ComputedFieldFunction qf inputArgs _ _ _ = _cfFunction computedFieldInfo
scalarArgs = map (_qptName . faType) $ toList inputArgs scalarArgs = map (_qptName . faType) $ toList inputArgs
in (, scalarArgs) <$> mkFuncArgsInp qf inputArgs in (, scalarArgs) <$> mkFuncArgsInp qf inputArgs

View File

@ -36,12 +36,13 @@ import qualified Language.GraphQL.Draft.Syntax as G
data ComputedFieldDefinition data ComputedFieldDefinition
= ComputedFieldDefinition = ComputedFieldDefinition
{ _cfdFunction :: !QualifiedFunction { _cfdFunction :: !QualifiedFunction
, _cfdTableArgument :: !(Maybe FunctionArgName) , _cfdTableArgument :: !(Maybe FunctionArgName)
, _cfdSessionArgument :: !(Maybe FunctionArgName)
} deriving (Show, Eq, Lift, Generic) } deriving (Show, Eq, Lift, Generic)
instance NFData ComputedFieldDefinition instance NFData ComputedFieldDefinition
instance Cacheable ComputedFieldDefinition instance Cacheable ComputedFieldDefinition
$(deriveJSON (aesonDrop 4 snakeCase) ''ComputedFieldDefinition) $(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields = True} ''ComputedFieldDefinition)
data AddComputedField data AddComputedField
= AddComputedField = AddComputedField
@ -64,6 +65,7 @@ runAddComputedField q = do
data ComputedFieldValidateError data ComputedFieldValidateError
= CFVENotValidGraphQLName !ComputedFieldName = CFVENotValidGraphQLName !ComputedFieldName
| CFVEInvalidTableArgument !InvalidTableArgument | CFVEInvalidTableArgument !InvalidTableArgument
| CFVEInvalidSessionArgument !InvalidSessionArgument
| CFVENotBaseReturnType !PGScalarType | CFVENotBaseReturnType !PGScalarType
| CFVEReturnTableNotFound !QualifiedTable | CFVEReturnTableNotFound !QualifiedTable
| CFVENoInputArguments | CFVENoInputArguments
@ -76,17 +78,26 @@ data InvalidTableArgument
| ITANotTable !QualifiedTable !FunctionTableArgument | ITANotTable !QualifiedTable !FunctionTableArgument
deriving (Show, Eq) deriving (Show, Eq)
data InvalidSessionArgument
= ISANotFound !FunctionArgName
| ISANotJSON !FunctionSessionArgument
deriving (Show, Eq)
showError :: QualifiedFunction -> ComputedFieldValidateError -> Text showError :: QualifiedFunction -> ComputedFieldValidateError -> Text
showError qf = \case showError qf = \case
CFVENotValidGraphQLName computedField -> CFVENotValidGraphQLName computedField ->
computedField <<> " is not valid GraphQL name" computedField <<> " is not valid GraphQL name"
CFVEInvalidTableArgument (ITANotFound argName) -> CFVEInvalidTableArgument (ITANotFound argName) ->
argName <<> " is not an input argument of " <> qf <<> " function" argName <<> " is not an input argument of the function " <>> qf
CFVEInvalidTableArgument (ITANotComposite functionArg) -> CFVEInvalidTableArgument (ITANotComposite functionArg) ->
showFunctionTableArgument functionArg <> " is not COMPOSITE type" showFunctionTableArgument functionArg <> " is not COMPOSITE type"
CFVEInvalidTableArgument (ITANotTable ty functionArg) -> CFVEInvalidTableArgument (ITANotTable ty functionArg) ->
showFunctionTableArgument functionArg <> " of type " <> ty showFunctionTableArgument functionArg <> " of type " <> ty
<<> " is not the table to which the computed field is being added" <<> " is not the table to which the computed field is being added"
CFVEInvalidSessionArgument (ISANotFound argName) ->
argName <<> " is not an input argument of the function " <>> qf
CFVEInvalidSessionArgument (ISANotJSON functionArg) ->
showFunctionSessionArgument functionArg <> " is not of type JSON"
CFVENotBaseReturnType scalarType -> CFVENotBaseReturnType scalarType ->
"the function " <> qf <<> " returning type " <> toSQLTxt scalarType "the function " <> qf <<> " returning type " <> toSQLTxt scalarType
<> " is not a BASE type" <> " is not a BASE type"
@ -101,6 +112,8 @@ showError qf = \case
showFunctionTableArgument = \case showFunctionTableArgument = \case
FTAFirst -> "first argument of the function " <>> qf FTAFirst -> "first argument of the function " <>> qf
FTANamed argName _ -> argName <<> " argument of the function " <>> qf FTANamed argName _ -> argName <<> " argument of the function " <>> qf
showFunctionSessionArgument = \case
FunctionSessionArgument argName _ -> argName <<> " argument of the function " <>> qf
addComputedFieldP2Setup addComputedFieldP2Setup
:: (QErrM m) :: (QErrM m)
@ -116,7 +129,7 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
either (throw400 NotSupported . showErrors) pure =<< MV.runValidateT (mkComputedFieldInfo) either (throw400 NotSupported . showErrors) pure =<< MV.runValidateT (mkComputedFieldInfo)
where where
inputArgNames = rfiInputArgNames rawFunctionInfo inputArgNames = rfiInputArgNames rawFunctionInfo
ComputedFieldDefinition function maybeTableArg = definition ComputedFieldDefinition function maybeTableArg maybeSessionArg = definition
functionReturnType = QualifiedPGType (rfiReturnTypeSchema rawFunctionInfo) functionReturnType = QualifiedPGType (rfiReturnTypeSchema rawFunctionInfo)
(rfiReturnTypeName rawFunctionInfo) (rfiReturnTypeName rawFunctionInfo)
(rfiReturnTypeType rawFunctionInfo) (rfiReturnTypeType rawFunctionInfo)
@ -166,10 +179,21 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
validateTableArgumentType FTAFirst $ faType firstArg validateTableArgumentType FTAFirst $ faType firstArg
pure FTAFirst pure FTAFirst
maybePGSessionArg <- sequence $ do
argName <- maybeSessionArg
return $ case findWithIndex (maybe False (argName ==) . faName) inputArgs of
Just (sessionArg, index) -> do
let functionSessionArg = FunctionSessionArgument argName index
validateSessionArgumentType functionSessionArg $ faType sessionArg
pure functionSessionArg
Nothing ->
MV.refute $ pure $ CFVEInvalidSessionArgument $ ISANotFound argName
let inputArgSeq = Seq.fromList $ dropTableArgument tableArgument inputArgs
let inputArgSeq = Seq.fromList $ dropTableAndSessionArgument tableArgument
maybePGSessionArg inputArgs
computedFieldFunction = computedFieldFunction =
ComputedFieldFunction function inputArgSeq tableArgument $ ComputedFieldFunction function inputArgSeq tableArgument maybePGSessionArg $
rfiDescription rawFunctionInfo rfiDescription rawFunctionInfo
pure $ ComputedFieldInfo computedField computedFieldFunction returnType comment pure $ ComputedFieldInfo computedField computedFieldFunction returnType comment
@ -185,6 +209,14 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
unless (table == typeTable) $ unless (table == typeTable) $
MV.dispute $ pure $ CFVEInvalidTableArgument $ ITANotTable typeTable tableArg MV.dispute $ pure $ CFVEInvalidTableArgument $ ITANotTable typeTable tableArg
validateSessionArgumentType :: (MV.MonadValidate [ComputedFieldValidateError] m)
=> FunctionSessionArgument
-> QualifiedPGType
-> m ()
validateSessionArgumentType sessionArg qpt = do
when (not . isJSONType . _qptName $ qpt) $
MV.dispute $ pure $ CFVEInvalidSessionArgument $ ISANotJSON sessionArg
showErrors :: [ComputedFieldValidateError] -> Text showErrors :: [ComputedFieldValidateError] -> Text
showErrors allErrors = showErrors allErrors =
"the computed field " <> computedField <<> " cannot be added to table " "the computed field " <> computedField <<> " cannot be added to table "
@ -192,12 +224,20 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
where where
reasonMessage = makeReasonMessage allErrors (showError function) reasonMessage = makeReasonMessage allErrors (showError function)
dropTableArgument :: FunctionTableArgument -> [FunctionArg] -> [FunctionArg] dropTableAndSessionArgument :: FunctionTableArgument
dropTableArgument tableArg inputArgs = -> Maybe FunctionSessionArgument -> [FunctionArg]
case tableArg of -> [FunctionArg]
FTAFirst -> tail inputArgs dropTableAndSessionArgument tableArg sessionArg inputArgs =
FTANamed argName _ -> let withoutTable = case tableArg of
filter ((/=) (Just argName) . faName) inputArgs FTAFirst -> tail inputArgs
FTANamed argName _ ->
filter ((/=) (Just argName) . faName) inputArgs
alsoWithoutSession = case sessionArg of
Nothing -> withoutTable
Just (FunctionSessionArgument name _) ->
filter ((/=) (Just name) . faName) withoutTable
in alsoWithoutSession
addComputedFieldToCatalog addComputedFieldToCatalog
:: MonadTx m :: MonadTx m

View File

@ -191,6 +191,7 @@ fromTableRowArgs pfx = toFunctionArgs . fmap toSQLExp
S.FunctionArgs positional named S.FunctionArgs positional named
toSQLExp (AETableRow Nothing) = S.SERowIden $ mkBaseTableAls pfx toSQLExp (AETableRow Nothing) = S.SERowIden $ mkBaseTableAls pfx
toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAls pfx) acc toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAls pfx) acc
toSQLExp (AESession s) = s
toSQLExp (AEInput s) = s toSQLExp (AEInput s) = s
-- posttgres ignores anything beyond 63 chars for an iden -- posttgres ignores anything beyond 63 chars for an iden

View File

@ -261,6 +261,7 @@ type TableAggFlds = TableAggFldsG S.SQLExp
data ArgumentExp a data ArgumentExp a
= AETableRow !(Maybe Iden) -- ^ table row accessor = AETableRow !(Maybe Iden) -- ^ table row accessor
| AESession !a -- ^ JSON/JSONB hasura session variable object
| AEInput !a | AEInput !a
deriving (Show, Eq, Functor, Foldable, Traversable) deriving (Show, Eq, Functor, Foldable, Traversable)

View File

@ -44,6 +44,18 @@ instance ToJSON FunctionTableArgument where
toJSON FTAFirst = String "first_argument" toJSON FTAFirst = String "first_argument"
toJSON (FTANamed argName _) = object ["name" .= argName] toJSON (FTANamed argName _) = object ["name" .= argName]
-- | The session argument, which passes Hasura session variables to a
-- SQL function as a JSON object.
data FunctionSessionArgument
= FunctionSessionArgument
!FunctionArgName -- ^ The argument name
!Int -- ^ The ordinal position in the function input parameters
deriving (Show, Eq, Generic)
instance Cacheable FunctionSessionArgument
instance ToJSON FunctionSessionArgument where
toJSON (FunctionSessionArgument argName _) = toJSON argName
data ComputedFieldReturn data ComputedFieldReturn
= CFRScalar !PGScalarType = CFRScalar !PGScalarType
| CFRSetofTable !QualifiedTable | CFRSetofTable !QualifiedTable
@ -58,10 +70,11 @@ $(makePrisms ''ComputedFieldReturn)
data ComputedFieldFunction data ComputedFieldFunction
= ComputedFieldFunction = ComputedFieldFunction
{ _cffName :: !QualifiedFunction { _cffName :: !QualifiedFunction
, _cffInputArgs :: !(Seq.Seq FunctionArg) , _cffInputArgs :: !(Seq.Seq FunctionArg)
, _cffTableArgument :: !FunctionTableArgument , _cffTableArgument :: !FunctionTableArgument
, _cffDescription :: !(Maybe PGDescription) , _cffSessionArgument :: !(Maybe FunctionSessionArgument)
, _cffDescription :: !(Maybe PGDescription)
} deriving (Show, Eq, Generic) } deriving (Show, Eq, Generic)
instance Cacheable ComputedFieldFunction instance Cacheable ComputedFieldFunction
$(deriveToJSON (aesonDrop 4 snakeCase) ''ComputedFieldFunction) $(deriveToJSON (aesonDrop 4 snakeCase) ''ComputedFieldFunction)

View File

@ -48,3 +48,32 @@
name: get_articles name: get_articles
response: response:
message: success message: success
- description: Add a computed field, passing the Hasura session argument
url: /v1/query
status: 200
query:
type: add_computed_field
args:
table: author
name: test_session
definition:
function: test_session
session_argument: session
response:
message: success
- description: obtain the session variable via computed fields
url: /v1/graphql
status: 200
query:
query: |
query {
author_by_pk(id: 1) {
test_session(args:{key:"x-hasura-role"})
}
}
response:
data:
author_by_pk:
test_session: admin

View File

@ -30,7 +30,6 @@
function: function:
schema: public schema: public
name: full_name name: full_name
table_argument:
name: first_name name: first_name
comment: comment:
table: table:
@ -41,6 +40,7 @@
path: $.args path: $.args
error: field definition conflicts with postgres column error: field definition conflicts with postgres column
code: constraint-violation code: constraint-violation
- description: Try adding computed field with invalid function - description: Try adding computed field with invalid function
url: /v1/query url: /v1/query
status: 400 status: 400
@ -58,7 +58,6 @@
function: function:
schema: public schema: public
name: random_function name: random_function
table_argument:
name: full_name name: full_name
comment: comment:
table: table:
@ -71,6 +70,7 @@
error: 'in table "author": in computed field "full_name": no such function exists error: 'in table "author": in computed field "full_name": no such function exists
in postgres : "random_function"' in postgres : "random_function"'
code: constraint-violation code: constraint-violation
- description: Try adding computed field with invalid table argument name - description: Try adding computed field with invalid table argument name
url: /v1/query url: /v1/query
status: 400 status: 400
@ -97,13 +97,14 @@
name: author name: author
reason: 'in table "author": in computed field "full_name": the computed field reason: 'in table "author": in computed field "full_name": the computed field
"full_name" cannot be added to table "author" because "random" is not an input "full_name" cannot be added to table "author" because "random" is not an input
argument of "full_name" function' argument of the function "full_name"'
type: computed_field type: computed_field
path: $.args path: $.args
error: 'in table "author": in computed field "full_name": the computed field "full_name" error: 'in table "author": in computed field "full_name": the computed field "full_name"
cannot be added to table "author" because "random" is not an input argument cannot be added to table "author" because "random" is not an input argument
of "full_name" function' of the function "full_name"'
code: constraint-violation code: constraint-violation
- description: Try adding computed field with a volatile function - description: Try adding computed field with a volatile function
url: /v1/query url: /v1/query
status: 400 status: 400
@ -132,15 +133,16 @@
\ field \"get_articles\" cannot be added to table \"author\" for the following\ \ field \"get_articles\" cannot be added to table \"author\" for the following\
\ reasons:\n • the function \"fetch_articles_volatile\" is of type VOLATILE;\ \ reasons:\n • the function \"fetch_articles_volatile\" is of type VOLATILE;\
\ cannot be added as a computed field\n • \"random\" is not an input argument\ \ cannot be added as a computed field\n • \"random\" is not an input argument\
\ of \"fetch_articles_volatile\" function\n" \ of the function \"fetch_articles_volatile\"\n"
type: computed_field type: computed_field
path: $.args path: $.args
error: "in table \"author\": in computed field \"get_articles\": the computed\ error: "in table \"author\": in computed field \"get_articles\": the computed\
\ field \"get_articles\" cannot be added to table \"author\" for the following\ \ field \"get_articles\" cannot be added to table \"author\" for the following\
\ reasons:\n • the function \"fetch_articles_volatile\" is of type VOLATILE;\ \ reasons:\n • the function \"fetch_articles_volatile\" is of type VOLATILE;\
\ cannot be added as a computed field\n • \"random\" is not an input argument\ \ cannot be added as a computed field\n • \"random\" is not an input argument\
\ of \"fetch_articles_volatile\" function\n" \ of the function \"fetch_articles_volatile\"\n"
code: constraint-violation code: constraint-violation
- description: Try adding a computed field with a function with no input arguments - description: Try adding a computed field with a function with no input arguments
url: /v1/query url: /v1/query
status: 400 status: 400
@ -158,7 +160,6 @@
function: function:
schema: public schema: public
name: hello_world name: hello_world
table_argument:
name: hello_world name: hello_world
comment: comment:
table: table:
@ -173,6 +174,7 @@
"hello_world" cannot be added to table "author" because the function "hello_world" "hello_world" cannot be added to table "author" because the function "hello_world"
has no input arguments' has no input arguments'
code: constraint-violation code: constraint-violation
- description: Try adding a computed field with first argument as table argument - description: Try adding a computed field with first argument as table argument
url: /v1/query url: /v1/query
status: 400 status: 400
@ -190,7 +192,6 @@
function: function:
schema: public schema: public
name: fetch_articles name: fetch_articles
table_argument:
name: get_articles name: get_articles
comment: comment:
table: table:
@ -209,3 +210,73 @@
\ type\n • first argument of the function \"fetch_articles\" of type \"pg_catalog.text\"\ \ type\n • first argument of the function \"fetch_articles\" of type \"pg_catalog.text\"\
\ is not the table to which the computed field is being added\n" \ is not the table to which the computed field is being added\n"
code: constraint-violation code: constraint-violation
- description: Try adding a computed field with an invalid session argument name
url: /v1/query
status: 400
query:
type: add_computed_field
args:
table: author
name: full_name
definition:
function: full_name
session_argument: random
response:
internal:
- definition:
definition:
function:
schema: public
name: full_name
session_argument: random
name: full_name
comment:
table:
schema: public
name: author
reason: 'in table "author": in computed field "full_name": the computed field
"full_name" cannot be added to table "author" because "random" is not an input
argument of the function "full_name"'
type: computed_field
path: $.args
error: 'in table "author": in computed field "full_name": the computed field "full_name"
cannot be added to table "author" because "random" is not an input argument
of the function "full_name"'
code: constraint-violation
- description: Try adding a computed field with a non-JSON session argument
url: /v1/query
status: 400
query:
type: add_computed_field
args:
table: author
name: fetch_articles
definition:
function: fetch_articles
table_argument: author_row
session_argument: search
response:
internal:
- definition:
definition:
function:
schema: public
name: fetch_articles
table_argument: author_row
session_argument: search
name: fetch_articles
comment:
table:
schema: public
name: author
reason: 'in table "author": in computed field "fetch_articles": the computed field
"fetch_articles" cannot be added to table "author" because "search" argument
of the function "fetch_articles" is not of type JSON'
type: computed_field
path: $.args
error: 'in table "author": in computed field "fetch_articles": the computed field
"fetch_articles" cannot be added to table "author" because "search" argument of
the function "fetch_articles" is not of type JSON'
code: constraint-violation

View File

@ -67,6 +67,11 @@ args:
SELECT 'Hello, World!'::text SELECT 'Hello, World!'::text
$$ LANGUAGE sql STABLE; $$ LANGUAGE sql STABLE;
CREATE FUNCTION test_session(author_row author, key text, session json)
RETURNS TEXT AS $$
SELECT $3->>$2
$$ LANGUAGE sql STABLE;
- type: track_table - type: track_table
args: args:
name: author name: author

View File

@ -7,6 +7,7 @@ args:
DROP FUNCTION fetch_articles(text, author); DROP FUNCTION fetch_articles(text, author);
DROP FUNCTION fetch_articles_volatile(text, author); DROP FUNCTION fetch_articles_volatile(text, author);
DROP FUNCTION full_name(author); DROP FUNCTION full_name(author);
DROP FUNCTION test_session(author, text, json);
DROP TABLE article; DROP TABLE article;
DROP TABLE author; DROP TABLE author;
cascade: true cascade: true

View File

@ -30,6 +30,12 @@ response:
schema: public schema: public
name: fetch_articles name: fetch_articles
table_argument: author_row table_argument: author_row
- name: test_session
definition:
function:
schema: public
name: test_session
session_argument: session
functions: functions:
- function: - function:
schema: public schema: public

View File

@ -122,6 +122,23 @@ args:
function: fetch_articles function: fetch_articles
table_argument: author_row table_argument: author_row
#Computed field with session argument
- type: run_sql
args:
sql: |
CREATE FUNCTION test_session(author_row author, key text, session json)
RETURNS TEXT AS $$
SELECT $3->>$2
$$ LANGUAGE sql STABLE;
- type: add_computed_field
args:
table: author
name: test_session
definition:
function: test_session
session_argument: session
- type: run_sql - type: run_sql
args: args:
sql: | sql: |

View File

@ -6,5 +6,5 @@ args:
cascade: true cascade: true
sql: | sql: |
drop table article cascade; drop table article cascade;
drop table author; drop table author cascade;
drop table text_result cascade; drop table text_result cascade;