graphql-engine/rfcs/function-root-field-customisation.md
Philip Lykke Carlsen c839fc55df Revise function-root-field-customisation.md
### Description
* Removed unresolved questions since resolved
* Change status 'draft' → 'published'

### Changelog

- [X] `no-changelog-required` .

### Related Issues

fix https://github.com/hasura/graphql-engine/issues/7405

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2496
GitOrigin-RevId: 0bdba7f5d52f2a4bd70c525dab12698e9ceddb28
2021-10-04 11:39:59 +00:00

8.3 KiB

Customising Function Root Field Names

Metadata

---
authors:
   Vamshi Surabhi <vamshi@hasura.io>,
   Gil Mizrahi <gil@hasura.io,
   Evie Ciobanu <evie@hasura.io>,
   Philip Lykke Carlsen <philip@hasura.io>
discussion:
   https://github.com/hasura/graphql-engine/issues/7405
state: published
---

Description

We should let users customise the GraphQL name that appears in the schema of SQL Functions tracked by The GraphQL Engine.

Problem

Being able to customise the names that tables are exposed under in the GraphQL schema is already supported by The GraphQL Engine, since PR-5719.

Citing the above PR for motivation:

.. The identifier can be useful to use when tracking tables with non GraphQL compliant names or it can be useful if the nodes that are generated should not be dependent on the table name i.e when a table is tracked with an identifier and if the table has been renamed, the node names will not be changed due to the identifier.

The same motivation applies for functions that are exposed in the GraphQL Schema.

Success

This feature will have been implemented fully when a user is able to, through the Console:

  • Track a function
  • Set a custom name for it
  • Use that name in queries

Similarly, the server-specific part will have been implemented once there is an integration test that performs the above steps via the server API.

What

The server part should add new metadata API endpoint(s) that allows a user, for some given tracked function, to specify a custom name that is used to query this function in the GraphQL Schema. In the absence of this feature, the name of the tracked function is just directly exposed in the GraphQL schema [1].

The console part should include a tab within the tracked function page that lets users specify the customised name, similarly to how custom table names are specified.

It's important to note that this change only affects the GraphQL frontend, leaving SQL generation untouched.

Also note that, since the Postgres backend is the only one that supports functions, the change described in this document is only in scope for Postgres. In the longer perspective, the query abstraction feature that will be implemented across backends will not be SQL Functions directly, but rather Stored Queries, which as of this writing have yet to be specified. (See Slack Discussion)

Effects and Interactions

This will require work on both Server and Console. Once the Server parts have been implemented the Console parts can be implemented.

This adds new metadata API endpoints. We don't perceive this feature to interfere with any other features.

Unresolved Questions

None. See linked discussion for historically unresolved questions.

How

Functions already have a configuration section - we can add two top level fields called custom_name and custom_root_fields as follows:

functions:
- function: function_name
  configuration:
    session_argument: ...
    exposed_as: ...
    custom_name: a custom graphql compliant name
    custom_root_fields:
      function: FunctionName
      function_aggregate: SomeOtherName
  permissions: ...

Console will require an equivalent of set_table_customization, maybe set_function_configuration to allow modifying this configuration after a function is tracked.

The configuration section of a function (FunctionMetadata b) needs two new options:

  1. custom_name is optional and expects a GraphQL name. custom_name should be used in place of the function's name when generating GraphQL fields or types for the function.

  2. custom_root_fields is optional and expects an object with the following two fields (both should be GraphQL names):

    1. function: This name should be used for the field name which exposes the function in the appropriate operation root (query/mutation/subscription). When this not specified, we use <custom_name/function_name> as the field name.

    2. function_aggregate: This name should be used for the aggregate operation that is exposed on the function. In the absence of this option, we use <custom_name/function_name>_aggregate as the field name.

    Note that both function and function_name take precedence over custom_name.

We do not expect functions support on other databases, so this is only expected to work on Postgres databases.

(These are equivalent to custom_name and custom_root_fields in a table's configuration section (TableConfig b))

In addition, we need an API for updating this configuration from the console. For table configuration, it is done through set_table_customization so we need something similar to that for functions too. I'm wondering if it makes sense to just add replace_if_exists key to track_function API instead of a set_function_customization API.

Examples

Example, consider the following schema:

create table article (
  id serial primary key,
  title text not null,
  content text not null
);
create function search_articles(key text)
returns setof articles AS $$
  select * from article
  where
    title ilike ('%' || key || '%')
    OR content ilike ('%' || key || '%')
$$ language sql stable;

with this metadata:

version: 3
sources:
- name: wb
  kind: postgres
  configuration:
    connection_info:
      database_url:
        from_env: WORKBENCH_URL
  tables:
  - table: article
  functions:
  - function: search_articles

We would have the following fields in query_root (considering only the fields of function search_articles):

search_articles(
  args: search_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): [article!]!
search_articles_aggregate(
  args: search_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): article_aggregate!

If custom_name is specified as follows:

version: 3
sources:
- name: wb
  kind: postgres
  configuration:
    connection_info:
      database_url:
        from_env: WORKBENCH_URL
  tables:
  - table: article
  functions:
  - function: search_articles
    configuration:
      custom_name: find_articles

the root fields would have the following schema:

find_articles(
  args: find_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): [article!]!
find_articles_aggregate(
  args: find_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): article_aggregate!

The changes are:

search_articles -> find_articles (root field name)
search_articles_aggregate -> find_articles_aggregate (root field name)
search_articles_args -> find_articles_args (input type name)

Instead, if custom_root_fields are specified as follows:

version: 3
sources:
- name: wb
  kind: postgres
  configuration:
    connection_info:
      database_url:
        from_env: WORKBENCH_URL
  tables:
  - table: article
  functions:
  - function: search_articles
    configuration:
      custom_name: find_articles
      custom_root_fields:
        function: FindArticles
        function_aggregate: FindArticlesAggregate

the generated schema should be:

FindArticles(
  args: find_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): [article!]!
FindArticlesAggregate(
  args: find_articles_args!
  distinct_on: [article_select_column!]
  limit: Int
  offset: Int
  order_by: [article_order_by!]
  where: article_bool_exp
  ): article_aggregate!

The changes are:

find_articles -> FindArticles (root field name)
find_articles_aggregate -> FindArticlesAggregate (root field name)

Future work

In general maybe we can extend most of our track_*/create_* APIs with replace_if_exists similar to CREATE OR REPLACE in Postgres?

Footnotes

[1]^: Subject to the name being made into a valid GraphQL identifier.