mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
Merge branch 'master' into init-current-directory
This commit is contained in:
commit
1002d20a0a
32
CHANGELOG.md
32
CHANGELOG.md
@ -2,6 +2,36 @@
|
||||
|
||||
## 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
|
||||
|
||||
- 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)
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- 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: 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)
|
||||
- console: show pre-release update notifications with opt out option (#3888)
|
||||
- console: handle invalid keys in permission builder (close #3848) (#3863)
|
||||
- docs: add page on data validation to docs (close #4085) (#4260)
|
||||
|
@ -105,6 +105,12 @@ ComputedFieldDefinition
|
||||
- String
|
||||
- Name of the argument which accepts a table row type. If omitted, the first
|
||||
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:
|
||||
|
||||
|
@ -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
|
||||
columns. Computed fields are computed when requested for via SQL functions using other columns of the table and other
|
||||
custom inputs if needed.
|
||||
columns. Computed fields are computed when requested for via `custom SQL functions <https://www.postgresql.org/docs/current/sql-createfunction.html>`__
|
||||
using other columns of the table and other custom inputs if needed.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -195,6 +195,76 @@ Computed fields permissions
|
||||
- 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
|
||||
----------------------------------------------
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
.. _custom_sql_functions:
|
||||
|
||||
Customise schema with SQL functions
|
||||
===================================
|
||||
Extend schema with SQL functions
|
||||
================================
|
||||
|
||||
.. contents:: Table of contents
|
||||
: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>`_
|
||||
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
|
||||
using both ``queries`` and ``subscriptions``.
|
||||
Hasura GraphQL engine lets you expose certain types of custom functions as top level fields in the GraphQL API to allow
|
||||
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
|
||||
-----------------------
|
||||
***********************
|
||||
|
||||
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>`__):
|
||||
|
||||
- **Function behaviour**: ONLY ``STABLE`` or ``IMMUTABLE``
|
||||
|
359
docs/graphql/manual/schema/data-validations.rst
Normal file
359
docs/graphql/manual/schema/data-validations.rst
Normal 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>`__.
|
@ -30,11 +30,12 @@ Postgres constructs.
|
||||
|
||||
Basics <basics>
|
||||
Relationships <relationships/index>
|
||||
Customise with views <views>
|
||||
Customise with SQL functions <custom-functions>
|
||||
Extend with views <views>
|
||||
Extend with SQL functions <custom-functions>
|
||||
Default field values <default-values/index>
|
||||
Enum type fields <enums>
|
||||
enums
|
||||
computed-fields
|
||||
custom-field-names
|
||||
data-validations
|
||||
Using an existing database <using-existing-database>
|
||||
Export GraphQL schema <export-graphql-schema>
|
||||
|
@ -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``.
|
||||
|
||||
.. _table_relationships:
|
||||
|
||||
Table relationships
|
||||
-------------------
|
||||
|
||||
@ -36,11 +38,13 @@ following types of table relationships:
|
||||
| | | - a ``tag`` can have many ``articles`` |
|
||||
+------------------+-----------------------------------+------------------------------------------------+
|
||||
|
||||
.. _graphql_relationships:
|
||||
|
||||
GraphQL schema relationships
|
||||
----------------------------
|
||||
|
||||
As you can see, each table relationship will have two component relationships (one in either direction) in the GraphQL
|
||||
schema. These relationships can be one of the following types:
|
||||
Each table relationship, as you can see from the above section, will have two component relationships
|
||||
(one in either direction) in the GraphQL schema. These relationships can be one of the following types:
|
||||
|
||||
+-----------------------------------------+------------------------------------------+---------------------------------------------------------------------------------------+
|
||||
| Type | Example | Meaning |
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
.. _custom_views:
|
||||
|
||||
Customise schema with views
|
||||
===========================
|
||||
Extend schema with views
|
||||
========================
|
||||
|
||||
.. contents:: Table of contents
|
||||
:backlinks: none
|
||||
|
BIN
docs/img/graphql/manual/schema/actions-data-validation.png
Executable file
BIN
docs/img/graphql/manual/schema/actions-data-validation.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
docs/img/graphql/manual/schema/validation-actions-def.png
Normal file
BIN
docs/img/graphql/manual/schema/validation-actions-def.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
docs/img/graphql/manual/schema/validation-author-isactive.png
Normal file
BIN
docs/img/graphql/manual/schema/validation-author-isactive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
docs/img/graphql/manual/schema/validation-not-empty.png
Normal file
BIN
docs/img/graphql/manual/schema/validation-not-empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
@ -62,7 +62,7 @@ resolveComputedField
|
||||
=> ComputedField -> Field -> m (RS.ComputedFieldSel UnresolvedVal)
|
||||
resolveComputedField computedField fld = fieldAsPath fld $ do
|
||||
funcArgs <- parseFunctionArgs argSeq argFn $ Map.lookup "args" $ _fArguments fld
|
||||
let argsWithTableArgument = withTableArgument funcArgs
|
||||
let argsWithTableArgument = withTableAndSessionArgument funcArgs
|
||||
case fieldType of
|
||||
CFTScalar scalarTy -> do
|
||||
colOpM <- argsToColOp $ _fArguments fld
|
||||
@ -73,16 +73,25 @@ resolveComputedField computedField fld = fieldAsPath fld $ do
|
||||
RS.CFSTable RS.JASMultipleRows <$> fromField functionFrom cols permFilter permLimit fld
|
||||
where
|
||||
ComputedField _ function argSeq fieldType = computedField
|
||||
ComputedFieldFunction qf _ tableArg _ = function
|
||||
ComputedFieldFunction qf _ tableArg sessionArg _ = function
|
||||
argFn :: FunctionArgItem -> InputFunctionArgument
|
||||
argFn = IFAUnknown
|
||||
withTableArgument resolvedArgs =
|
||||
withTableAndSessionArgument :: RS.FunctionArgsExpG UnresolvedVal
|
||||
-> RS.FunctionArgsExpTableRow UnresolvedVal
|
||||
withTableAndSessionArgument resolvedArgs =
|
||||
let argsExp@(RS.FunctionArgsExp positional named) = RS.AEInput <$> resolvedArgs
|
||||
tableRowArg = RS.AETableRow Nothing
|
||||
in case tableArg of
|
||||
FTAFirst ->
|
||||
RS.FunctionArgsExp (tableRowArg:positional) named
|
||||
FTANamed argName index ->
|
||||
RS.insertFunctionArg argName index tableRowArg argsExp
|
||||
withTable = case tableArg of
|
||||
FTAFirst ->
|
||||
RS.FunctionArgsExp (tableRowArg:positional) named
|
||||
FTANamed argName index ->
|
||||
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
|
||||
:: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r
|
||||
|
@ -315,7 +315,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi
|
||||
|
||||
-- computed fields' function args input objects and scalar types
|
||||
mkComputedFieldRequiredTypes computedFieldInfo =
|
||||
let ComputedFieldFunction qf inputArgs _ _ = _cfFunction computedFieldInfo
|
||||
let ComputedFieldFunction qf inputArgs _ _ _ = _cfFunction computedFieldInfo
|
||||
scalarArgs = map (_qptName . faType) $ toList inputArgs
|
||||
in (, scalarArgs) <$> mkFuncArgsInp qf inputArgs
|
||||
|
||||
|
@ -36,12 +36,13 @@ import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
data ComputedFieldDefinition
|
||||
= ComputedFieldDefinition
|
||||
{ _cfdFunction :: !QualifiedFunction
|
||||
, _cfdTableArgument :: !(Maybe FunctionArgName)
|
||||
{ _cfdFunction :: !QualifiedFunction
|
||||
, _cfdTableArgument :: !(Maybe FunctionArgName)
|
||||
, _cfdSessionArgument :: !(Maybe FunctionArgName)
|
||||
} deriving (Show, Eq, Lift, Generic)
|
||||
instance NFData ComputedFieldDefinition
|
||||
instance Cacheable ComputedFieldDefinition
|
||||
$(deriveJSON (aesonDrop 4 snakeCase) ''ComputedFieldDefinition)
|
||||
$(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields = True} ''ComputedFieldDefinition)
|
||||
|
||||
data AddComputedField
|
||||
= AddComputedField
|
||||
@ -64,6 +65,7 @@ runAddComputedField q = do
|
||||
data ComputedFieldValidateError
|
||||
= CFVENotValidGraphQLName !ComputedFieldName
|
||||
| CFVEInvalidTableArgument !InvalidTableArgument
|
||||
| CFVEInvalidSessionArgument !InvalidSessionArgument
|
||||
| CFVENotBaseReturnType !PGScalarType
|
||||
| CFVEReturnTableNotFound !QualifiedTable
|
||||
| CFVENoInputArguments
|
||||
@ -76,17 +78,26 @@ data InvalidTableArgument
|
||||
| ITANotTable !QualifiedTable !FunctionTableArgument
|
||||
deriving (Show, Eq)
|
||||
|
||||
data InvalidSessionArgument
|
||||
= ISANotFound !FunctionArgName
|
||||
| ISANotJSON !FunctionSessionArgument
|
||||
deriving (Show, Eq)
|
||||
|
||||
showError :: QualifiedFunction -> ComputedFieldValidateError -> Text
|
||||
showError qf = \case
|
||||
CFVENotValidGraphQLName computedField ->
|
||||
computedField <<> " is not valid GraphQL name"
|
||||
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) ->
|
||||
showFunctionTableArgument functionArg <> " is not COMPOSITE type"
|
||||
CFVEInvalidTableArgument (ITANotTable ty functionArg) ->
|
||||
showFunctionTableArgument functionArg <> " of type " <> ty
|
||||
<<> " 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 ->
|
||||
"the function " <> qf <<> " returning type " <> toSQLTxt scalarType
|
||||
<> " is not a BASE type"
|
||||
@ -101,6 +112,8 @@ showError qf = \case
|
||||
showFunctionTableArgument = \case
|
||||
FTAFirst -> "first argument of the function " <>> qf
|
||||
FTANamed argName _ -> argName <<> " argument of the function " <>> qf
|
||||
showFunctionSessionArgument = \case
|
||||
FunctionSessionArgument argName _ -> argName <<> " argument of the function " <>> qf
|
||||
|
||||
addComputedFieldP2Setup
|
||||
:: (QErrM m)
|
||||
@ -116,7 +129,7 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
|
||||
either (throw400 NotSupported . showErrors) pure =<< MV.runValidateT (mkComputedFieldInfo)
|
||||
where
|
||||
inputArgNames = rfiInputArgNames rawFunctionInfo
|
||||
ComputedFieldDefinition function maybeTableArg = definition
|
||||
ComputedFieldDefinition function maybeTableArg maybeSessionArg = definition
|
||||
functionReturnType = QualifiedPGType (rfiReturnTypeSchema rawFunctionInfo)
|
||||
(rfiReturnTypeName rawFunctionInfo)
|
||||
(rfiReturnTypeType rawFunctionInfo)
|
||||
@ -166,10 +179,21 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
|
||||
validateTableArgumentType FTAFirst $ faType firstArg
|
||||
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 function inputArgSeq tableArgument $
|
||||
ComputedFieldFunction function inputArgSeq tableArgument maybePGSessionArg $
|
||||
rfiDescription rawFunctionInfo
|
||||
|
||||
pure $ ComputedFieldInfo computedField computedFieldFunction returnType comment
|
||||
@ -185,6 +209,14 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
|
||||
unless (table == typeTable) $
|
||||
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 allErrors =
|
||||
"the computed field " <> computedField <<> " cannot be added to table "
|
||||
@ -192,12 +224,20 @@ addComputedFieldP2Setup trackedTables table computedField definition rawFunction
|
||||
where
|
||||
reasonMessage = makeReasonMessage allErrors (showError function)
|
||||
|
||||
dropTableArgument :: FunctionTableArgument -> [FunctionArg] -> [FunctionArg]
|
||||
dropTableArgument tableArg inputArgs =
|
||||
case tableArg of
|
||||
FTAFirst -> tail inputArgs
|
||||
FTANamed argName _ ->
|
||||
filter ((/=) (Just argName) . faName) inputArgs
|
||||
dropTableAndSessionArgument :: FunctionTableArgument
|
||||
-> Maybe FunctionSessionArgument -> [FunctionArg]
|
||||
-> [FunctionArg]
|
||||
dropTableAndSessionArgument tableArg sessionArg inputArgs =
|
||||
let withoutTable = case tableArg of
|
||||
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
|
||||
:: MonadTx m
|
||||
|
@ -191,6 +191,7 @@ fromTableRowArgs pfx = toFunctionArgs . fmap toSQLExp
|
||||
S.FunctionArgs positional named
|
||||
toSQLExp (AETableRow Nothing) = S.SERowIden $ mkBaseTableAls pfx
|
||||
toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAls pfx) acc
|
||||
toSQLExp (AESession s) = s
|
||||
toSQLExp (AEInput s) = s
|
||||
|
||||
-- posttgres ignores anything beyond 63 chars for an iden
|
||||
|
@ -261,6 +261,7 @@ type TableAggFlds = TableAggFldsG S.SQLExp
|
||||
|
||||
data ArgumentExp a
|
||||
= AETableRow !(Maybe Iden) -- ^ table row accessor
|
||||
| AESession !a -- ^ JSON/JSONB hasura session variable object
|
||||
| AEInput !a
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
|
@ -44,6 +44,18 @@ instance ToJSON FunctionTableArgument where
|
||||
toJSON FTAFirst = String "first_argument"
|
||||
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
|
||||
= CFRScalar !PGScalarType
|
||||
| CFRSetofTable !QualifiedTable
|
||||
@ -58,10 +70,11 @@ $(makePrisms ''ComputedFieldReturn)
|
||||
|
||||
data ComputedFieldFunction
|
||||
= ComputedFieldFunction
|
||||
{ _cffName :: !QualifiedFunction
|
||||
, _cffInputArgs :: !(Seq.Seq FunctionArg)
|
||||
, _cffTableArgument :: !FunctionTableArgument
|
||||
, _cffDescription :: !(Maybe PGDescription)
|
||||
{ _cffName :: !QualifiedFunction
|
||||
, _cffInputArgs :: !(Seq.Seq FunctionArg)
|
||||
, _cffTableArgument :: !FunctionTableArgument
|
||||
, _cffSessionArgument :: !(Maybe FunctionSessionArgument)
|
||||
, _cffDescription :: !(Maybe PGDescription)
|
||||
} deriving (Show, Eq, Generic)
|
||||
instance Cacheable ComputedFieldFunction
|
||||
$(deriveToJSON (aesonDrop 4 snakeCase) ''ComputedFieldFunction)
|
||||
|
@ -48,3 +48,32 @@
|
||||
name: get_articles
|
||||
response:
|
||||
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
|
||||
|
@ -30,7 +30,6 @@
|
||||
function:
|
||||
schema: public
|
||||
name: full_name
|
||||
table_argument:
|
||||
name: first_name
|
||||
comment:
|
||||
table:
|
||||
@ -41,6 +40,7 @@
|
||||
path: $.args
|
||||
error: field definition conflicts with postgres column
|
||||
code: constraint-violation
|
||||
|
||||
- description: Try adding computed field with invalid function
|
||||
url: /v1/query
|
||||
status: 400
|
||||
@ -58,7 +58,6 @@
|
||||
function:
|
||||
schema: public
|
||||
name: random_function
|
||||
table_argument:
|
||||
name: full_name
|
||||
comment:
|
||||
table:
|
||||
@ -71,6 +70,7 @@
|
||||
error: 'in table "author": in computed field "full_name": no such function exists
|
||||
in postgres : "random_function"'
|
||||
code: constraint-violation
|
||||
|
||||
- description: Try adding computed field with invalid table argument name
|
||||
url: /v1/query
|
||||
status: 400
|
||||
@ -97,13 +97,14 @@
|
||||
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 "full_name" function'
|
||||
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 "full_name" function'
|
||||
of the function "full_name"'
|
||||
code: constraint-violation
|
||||
|
||||
- description: Try adding computed field with a volatile function
|
||||
url: /v1/query
|
||||
status: 400
|
||||
@ -132,15 +133,16 @@
|
||||
\ field \"get_articles\" cannot be added to table \"author\" for the following\
|
||||
\ 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\
|
||||
\ of \"fetch_articles_volatile\" function\n"
|
||||
\ of the function \"fetch_articles_volatile\"\n"
|
||||
type: computed_field
|
||||
path: $.args
|
||||
error: "in table \"author\": in computed field \"get_articles\": the computed\
|
||||
\ field \"get_articles\" cannot be added to table \"author\" for the following\
|
||||
\ 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\
|
||||
\ of \"fetch_articles_volatile\" function\n"
|
||||
\ of the function \"fetch_articles_volatile\"\n"
|
||||
code: constraint-violation
|
||||
|
||||
- description: Try adding a computed field with a function with no input arguments
|
||||
url: /v1/query
|
||||
status: 400
|
||||
@ -158,7 +160,6 @@
|
||||
function:
|
||||
schema: public
|
||||
name: hello_world
|
||||
table_argument:
|
||||
name: hello_world
|
||||
comment:
|
||||
table:
|
||||
@ -173,6 +174,7 @@
|
||||
"hello_world" cannot be added to table "author" because the function "hello_world"
|
||||
has no input arguments'
|
||||
code: constraint-violation
|
||||
|
||||
- description: Try adding a computed field with first argument as table argument
|
||||
url: /v1/query
|
||||
status: 400
|
||||
@ -190,7 +192,6 @@
|
||||
function:
|
||||
schema: public
|
||||
name: fetch_articles
|
||||
table_argument:
|
||||
name: get_articles
|
||||
comment:
|
||||
table:
|
||||
@ -209,3 +210,73 @@
|
||||
\ 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"
|
||||
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
|
||||
|
@ -67,6 +67,11 @@ args:
|
||||
SELECT 'Hello, World!'::text
|
||||
$$ 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
|
||||
args:
|
||||
name: author
|
||||
|
@ -7,6 +7,7 @@ args:
|
||||
DROP FUNCTION fetch_articles(text, author);
|
||||
DROP FUNCTION fetch_articles_volatile(text, author);
|
||||
DROP FUNCTION full_name(author);
|
||||
DROP FUNCTION test_session(author, text, json);
|
||||
DROP TABLE article;
|
||||
DROP TABLE author;
|
||||
cascade: true
|
||||
|
@ -30,6 +30,12 @@ response:
|
||||
schema: public
|
||||
name: fetch_articles
|
||||
table_argument: author_row
|
||||
- name: test_session
|
||||
definition:
|
||||
function:
|
||||
schema: public
|
||||
name: test_session
|
||||
session_argument: session
|
||||
functions:
|
||||
- function:
|
||||
schema: public
|
||||
|
@ -122,6 +122,23 @@ args:
|
||||
function: fetch_articles
|
||||
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
|
||||
args:
|
||||
sql: |
|
||||
|
@ -6,5 +6,5 @@ args:
|
||||
cascade: true
|
||||
sql: |
|
||||
drop table article cascade;
|
||||
drop table author;
|
||||
drop table author cascade;
|
||||
drop table text_result cascade;
|
||||
|
Loading…
Reference in New Issue
Block a user