Smart Tags

You can customise your PostGraphile GraphQL schema by tagging tables, columns, functions, relations, etc. These changes could be renaming something, omitting things from your GraphQL schema, or anything else a plugin supports!

We call this functionality "Smart Tags" and it allows you to easily customise the generated GraphQL schema without making breaking changes to your database.

If you're using PostGraphile in --watch mode, you should be able to see in PostGraphile's GraphiQL client that the related types and fields will reflect the change almost immediately. If you're not using --watch then you may need to restart the server for smart tag changes to take effect.

The @ character

We often refer to things like the @omit smart tag or the @name smart tag; really these tags are just omit and name respectively; but in the Smart Comment syntax (the oldest form of smart tags in PostGraphile) the @ is required to denote a smart tag, and this pattern has stuck when referring to smart tags.

You will also often see the smart comment syntax used to refer to smart tags in general, it's because the syntax is a little easier to write quickly. Read the Smart Comments Spec and you'll soon learn to translate @omit update,delete to omit: "update,delete" (and vice-versa) in your head.

Valid values

Tags have a limited set of valid values:

  • true
  • a string
  • an array of strings

Some tags only support one or two of these types of values.

Ways of adding tags

There's multiple ways of adding tags to entities:

Example

In this example we're going to use Smart Comments for brevity, but this works with all the ways of adding smart tags.

Here is a basic table, with the name changed from original_table to renamed_table:

create table original_table (
  col1 int
);

comment on table original_table is E'@name renamed_table';

The column can also be renamed:

comment on column original_table.col1 is E'@name colA';

The same can be done for types and custom queries:

create type flibble as (f text);

create function getFlamble() returns SETOF flibble as $$
    select (body)::flibble from post
$$ language sql;

comment on type flibble is E'@name flamble';
comment on function getFlamble() is E'@name allFlambles';

Smart comments are also reflected in GraphiQL. Here, we are querying the table original_table by looking at allOriginalTables:

Next, we add the smart comment @name renamed_table on original_table and the rename is instantly reflected in GraphiQL:

So now the query needs to use the new name for the table:

Built-in smart tags

Any plugin may implement support for smart tags, so refer to those plugins' documentation. The following are smart tags built into PostGraphile.

@deprecated

You can deprecate a database column using the deprecated tag. If you need multiple lines, you can specify the tag multiple times, one per line of output text.

Applies to:

  • columns
attribute: {
  my_column: {
    tags: {
      deprecated: "Use myOtherColumn instead."
    }
  }
}
comment on column my_schema.my_table.my_column is
  E'@deprecated Use myOtherColumn instead.';

@hasDefault

You can indicate that a column has a default - be that implemented via a trigger, or maybe as an instead of insert rule - so the field will be nullable in create* mutations.

comment on column my_schema.my_table.my_column is E'@hasDefault';

@name

You can add a smart tag to an entity to rename that entity. For tables, columns, custom types and many functions you can use the name tag. For more complex things we use different tags, such as for foreign key constraints we have fieldName and foreignFieldName.

Applies to:

  • Tables
  • Views
  • Materialized views
  • Composite types (one direction only)
  • Columns
  • Types
  • Custom Query functions: the Query field name
  • Custom Mutation functions: the Mutation field name
class: {
  post: {
    tags: {
      name: "message"
    }
  }
},
procedure: {
  search_posts: {
    tags: {
      name: "returnPostsMatching"
    }
  }
}
comment on table post is
  E'@name message';
comment on function search_posts(text) is
  E'@name returnPostsMatching';

@fieldName

Applies to:

  • foreign key constraints: the local field name for the relationship (see also @foreignFieldName)
  • unique constraints: the root finder field name
  • computed column functions: the field name this function creates

@foreignFieldName

foreignFieldName applies to

  • foreign key constraints: the field on the remote type (the "backwards" relation)
foreignFieldName: "threads",
fieldName: "author"

See also: @fieldName

@resultFieldName

Applies to:

procedure: {
  authenticate: {
    tags: {
      name: "login",
      resultFieldName: "token",
    }
  }
}
comment on function authenticate(text, text) is
  E'@resultFieldName token\n@name login';

@omit

To remove an entity from your API, you can use the 'omit' smart tag. If you only want to omit the entity from certain operations you can list them. For example, @omit update on a table would prevent the table from having an update-related functionality whilst still including queries, create and delete. @omit update on a column would prevent the column appearing in the Patch type, so it cannot be updated (but can still be created) via GraphQL.

Here's a quick-reference for the operations we currently support (you'll want to experiment with them as there wasn't space to put all the caveats in the table!):

Action Table effect Column effect Function effect
C create omit create mutation omit from create -
R read omit completely completely omitted -
U update omit update mutations omit from update -
D delete omit delete mutations - -
F filter omit condition arg omit from condition no filtering
O order omit orderBy arg omit from orderBy no ordering
A all no allFoos query - -
M many no foreign key fields - -
X execute - - function not present

Warning: This functionality is not intended for implementing permissions, it's for removing things from your API that you don't need. You should back these up with database permissions if needed.

Multiple actions can be listed using commas (no spaces!), as in the following example which disables mutations on a table:

class: {
  table_name: {
    tags: {
      omit: "create,update,delete"
    }
  }
}
comment on table table_name is E'@omit create,update,delete';

Applies to:

  • Tables
  • Views
  • Materialized views
  • Composite types (one direction only)
  • Columns
  • Custom Queries
  • Custom Mutations
  • Computed Columns
  • Unique constraints
  • Foreign key constraints
Example

On a simple table called book we have added a smart comment omitting the update and delete operations:

create table forum_example.book (
  col1 int
);

comment on table forum_example.book is E'@omit update,delete';

The results are immediately reflected in GraphiQL. We can also disable create operations:

comment on table forum_example.book is E'@omit create,update,delete';

On the left, you can see the documentation for all the fields and types regarding book before the create operation was omitted. On the right, you can see the reduced fields and types once the create operation is omitted.

GraphiQL displaying an omit smart comment example

@sortable

Since version v4.3.1

Applies to:

  • Functions returning SETOF a table-like composite type (e.g. a table type, view, materialized view, etc, but not a custom type): adds the orderBy argument to this connection, allowing to sort the set by its columns/attributes.
  • Computed column functions with no required parameters and returning a scalar or array: allows this function to be used as an order in the orderBy argument for the parent table, allowing to sort the parent table by this function's return value. parent table.

If used on a computed function that returns a SETOF record type it will make the connection sortable (like it would with any other SETOF function) and NOT a sort by for its parent table

comment on function foo() is E'@sortable';
comment on function users_foo(users) is E'@sortable';
{
  # If the function returns a set of table rows
  foo(orderBy: [ID_ASC]) { ... }
  user(nodeId: ...) {
    foo(orderBy: [ID_ASC]) { ... }
  }

  # If the function return a scalar
  allUsers(orderBy: [FOO_ASC]) { ... }
}

@filterable

Since version v4.3.1.

Applies to:

  • Functions returning SETOF a table-like composite type (e.g. a table type, view, materialized view, etc, but not a custom type): adds the condition argument to this connection, allowing to filter the set by any of its scalar fields.
  • Computed column functions with no required parameters and returning a scalar or array: allows this function to be used as field in the condition argument for the parent table, allowing to filter by this function's value the parent table.

If used on a computed function that returns a SETOF record type it will make the connection filterable (like it would with any other SETOF function) and NOT a filter by for its parent table

comment on function foo() is E'@filterable';
comment on function users_foo(users) is E'@filterable';
{
  # If the function returns a set of table rows
  foo(condition: {firstName: "Alice"}) { ... }

  # If the function return a scalar
  allUsers(condition: {foo: "FOO_VALUE"}) { ... }
}
Sorting and filtering non-scalar computed columns

If your computed column is returning a composite type, the recommended approach is to wrap it with a computed column that returns the scalar field you want to sort and/or filter by. For example:

-- non scalar function
CREATE OR REPLACE FUNCTION user_object(user user) RETURNS object AS $$
SELECT * FROM object where id = user.object_id;
$$ language SQL STABLE;

-- wrapper. Note the () for notation. Failing to use them will throw an error
CREATE OR REPLACE FUNCTION user_object_field(user user) RETURNS varchar AS $$
SELECT (user_object(user)).field;
$$ language SQL STABLE;

-- don't forget the comments...
comment on function user_object_field() is E'@sortable';

Note that this will only work with a single composite type. If your computed function returns a SETOF composite type one option is to use an ARRAY and the connection-filter plugin.

-- non scalar setof function
CREATE OR REPLACE FUNCTION user_objects(user user) RETURNS SETOF object AS $$
SELECT * FROM object where owner_id = user.id; -- one user, many objects
$$ language SQL STABLE;

-- wrapper. Note the () for notation. Failing to use them will throw an error
CREATE OR REPLACE FUNCTION user_object_fields(user user) RETURNS varchar[] AS $$
SELECT ARRAY_AGG((user_objects(user)).field);
$$ language SQL STABLE;

-- don't forget the comments...
comment on function user_object_fields() is E'@sortable';

Now you can use the array as a filter by, for example:

{
  # get all users who own an object with field == 'foo'
  allUsers(filter: {object_fields: contains: 'foo'}) { ... }
}

@simpleCollections

You can control whether simple collections are enabled by default using --simple-collections omit|both|only (or simpleCollections: "omit"|"both"|"only"); however sometimes you want to override this on a case by case setting - for example if you want relay connections for almost all collections, except when it comes to a user's email addresses where you want to use a simple list.

You can do this with the @simpleCollections omit, @simpleCollections both and @simpleCollections only smart comments.

Applies to:

  • Tables
  • Views
  • Materialized views
  • Custom Query functions
  • Computed Column functions
comment on table email is
  E'@simpleCollections both';
comment on constraint email_user_id_fkey on email is
  E'@simpleCollections both';
comment on function search_people(query text) is
  E'@simpleCollections both';

@arg0variant, @arg1variant, ...

When building a custom mutation, you probably want to use the composite type that is generated when creating a table in PostgreSQL as a function argument, like this (note this is just an example for illustrative purposes):

create table example(
  id uuid primary key,
  name text not null
);

create function new_example(input example) returns example as $$
  insert into example (id, name) values (input.id, input.name) returning *;
$$ language sql volatile;

By default, composite types will be translated into a GraphQL types by PostGraphile with the same characteristics, i.e. all not null columns will become non-nullable fields. You can let PostGraphile know that you want to convert the composite type into another "variant" GraphQL type with a smart comment. Variants include patch (which is equivalent to the argument to update* mutations) and base (which makes every column both available (ignores permissions) and nullable). For example:

create table example(
  id uuid primary key,
  name text not null
);

create function new_example_with_auto_id(input example) returns example as $$
  insert into example (id, name) values (gen_random_uuid(), input.name) returning *;
$$ language sql volatile;

comment on function new_example_with_auto_id(input example) is
  E'@arg0variant patch';

This uses the patch variant from PostGraphile's update mutations which has all the fields except id. This will mean that the custom mutation will not ask for the id on the client-side anymore (because it will generate it itself). Note how arg0 refers to the first function parameter (we use a 0-indexed counter of the arguments), thus arg2 would be the third parameter.

Applies to:

  • Custom Query functions
  • Custom Mutation functions
  • Computed Column functions

Tags to add virtual constraint

You can add "virtual" (fake) constraints to types in PostgreSQL using smart comments. The primary use case for this is to make views act more table-like - allowing you to express the connections between tables and views. It's also useful on composite types.

@notNull

Allows marking the column as non-nullable.

Applies to:

  • columns
attribute: {
  "my_view.my_column": {
    tags: {
      notNull: true
    }
  }
}
comment on column my_view.my_column is E'@notNull`;
@primaryKey

Primary key columns will automatically be marked as @notNull, as they would in PostgreSQL.

If you declare something as a primary key it must be unique. We do not check it's unique - we trust you - but if it isn't unique then we're not sure what will happen...

class: {
  my_view: {
    tags: {
      primaryKey: "id"
      // or:
      //   primaryKey: "type,identifier"
    }
  }
}
comment on view my_view is E'@primaryKey id';
-- or
comment on view my_view is E'@primaryKey type,identifier';
@foreignKey

The foreign key adds virtual constraints pretending to be foreign keys. It has the following syntax which mirrors the PostgreSQL foreign key constraint:

@foreignKey (col1, ...) references [my_schema.]my_table [(col1, ...)]

In the tags file you must omit the leading @foreignKey text since it is specified as the key (rather than the value) in the tags object ─ see example below.

The schema is optional if the target table is in the same schema. If you're referencing a Primary Key on the remote table/view then you can skip the final column specification should you wish. Otherwise, you must reference columns matching a unique constraint.

Applies to:

  • Tables
  • Views
  • Materialized views
  • Composite types (one direction only)
class: {
  my_materialized_view: {
    tags: {
      foreignKey: "(key_1, key_2) references other_table (key_1, key_2)"
    }
  }
}

// or if you want multiple foreignKeys
class: {
  my_materialized_view: {
    tags: {
      foreignKey: [
        "(key_1, key_2) references other_table (key_1, key_2)",
        "(key_3, key_4) references some_other_table (key_3, key_4)"
      ]
    }
  }
}
comment on materialized view my_materialized_view is E'@foreignKey (key_1, key_2) references other_table (key_1, key_2)';

-- or if you want multiple foreignKeys
comment on materialized view my_materialized_view is E'@foreignKey (key_1, key_2) references other_table (key_1, key_2)\n@foreignKey (key_3, key_4) references some_other_table (key_3, key_4)';
@unique

From PostGraphile 4.9.1

Introduces a "fake" unique constraint, so @unique col1,col2 is somewhat equivalent to the following, except it can also be applied to entities that cannot have unique constraints, e.g. views. It is up to you to ensure that your data is indeed unique on the given columns.

-- `@unique col1,col2` is roughly equivalent to:
ALTER TABLE foo ADD CONSTRAINT fake_unique UNIQUE (col1, col2);

More than one @unique tag may be specified.

class: {
  my_view: {
    tags: {
      unique: [
        "id",
        "org_id,slug"
      ]
      // or:
      //   unique: "id"
    }
  }
}
comment on view my_view is E'@unique id\n@unique org,slug';
Smart Tags on virtual constraints

You can also add smart tags on virtual constraints, for example adding the fieldName smart tag to a virtual foreign key constraint, by appending the pipe character | followed by the @-prefixed smart tag:

class: {
  my_materialized_view: {
    tags: {
      foreignKey: "(key_1, key_2) references other_table (key_1, key_2)|@fieldName field_1"
    }
  }
}