Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polymorphic Resolver Schema Extension Question #1168

Closed
1 of 3 tasks
tazsingh opened this issue Oct 22, 2019 · 3 comments
Closed
1 of 3 tasks

Polymorphic Resolver Schema Extension Question #1168

tazsingh opened this issue Oct 22, 2019 · 3 comments

Comments

@tazsingh
Copy link

I'm submitting a ...

  • bug report
  • feature request
  • question

PostGraphile version: 4.4.4

Minimal SQL file that can be loaded into a clean database:

CREATE TABLE users (
  id integer PRIMARY KEY
);

CREATE TYPE groups (
  id integer PRIMARY KEY
);

CREATE TABLE slugs (
  id varchar(255) PRIMARY KEY,
  sluggable_id integer NOT NULL,
  sluggable_type varchar(255) NOT NULL CHECK (sluggable_type::text = ANY (ARRAY['users'::text, 'groups'::text])::text[])
);

INSERT INTO users (id) VALUES (1);
INSERT INTO groups (id) VALUES (1);

INSERT INTO slugs (id, sluggable_id, sluggable_type) VALUES ('taz', 1, 'users');
INSERT INTO slugs (id, sluggable_id, sluggable_type) VALUES ('my-awesome-group', 1, 'groups');

Steps to reproduce:

When extending the schema as such:

import { makeExtendSchemaPlugin, gql } from 'graphile-utils'

makeExtendSchemaPlugin(({ pgSql: sql, graphql: { getNamedType }, $$nodeType }) => {

  return {
    typeDefs: gql`
      union SluggableNode = User | Group

      extend type Query {
        nodeBySlugId(id: String!): SluggableNode @pgField
      }
    `,
    resolvers: {
      SluggableNode: {
        __resolveType(obj) {
          return obj[$$nodeType]
        },
      },
      Query: {
        async nodeBySlugId(root, { id }, { pgClient } , { graphile }) {
          const {
            rows: [slug],
          } = await pgClient.query(
            `select * from slugs where id = $1`,
            [id]
          )

          if (!slug) {
            throw new Error('Slug for Node not found')
          }

          console.log('SLUG IDENTIFIER')
          console.log(slug.identifier_id)

          const [
            node,
          ] = await graphile.selectGraphQLResultFromTable(
            sql.identifier(slug.sluggable_type),
            (tableAlias, queryBuilder) => {
              queryBuilder.where(
                sql.fragment`${tableAlias}.id = ${sql.value(
                  slug.sluggable_id
                )}`
              )
            }
          )

          if (node) {
            Object.defineProperty(node, $$nodeType, {
              enumerable: false,
              configurable: false,
              value: getNamedType(slug.sluggable_type === 'users' ? 'User' : 'Group'),
            })

            return node
          }

          return null
        },
      },
    },
  }
})

Current behaviour:

If I were to call:

{
  nodeBySlugId(id: 'taz') {
    ... on User {
      id
    }
  }
}

I receive:

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field User.id.",
      "locations": [
        {
          "line": 4,
          "column": 7
        }
      ],
      "path": [
        "nodeBySlugId",
        "id"
      ]
    }
  ],
  "data": {
    "nodeBySlugId": null
  }
}

Expected behaviour:

I should receive the ID for that referenced User:

{
  "data": {
    "nodeBySlugId": {
      "id": 1
    }
  }
}

General Notes for my Question

I'm attempting to have a table that houses all URL friendly strings such that I can have a /:slug URL that can then map to all of these entities.

From what I can see, PostGraphile is generating the correct query to get the node, however it's resulting in an empty object (i.e. {}) as the SELECT part of the query is empty (i.e. SELECT FROM users).

This leads me to believe that PostGraphile isn't "seeing" the fields that I've requested, likely due to the way that I'm performing this query?

Kindly note that I've extracted the above code from my application and thus it may not be 100% correct as I attempted to create a small reproducible example. However, it should provide a picture of the problem I'm facing and my attempt at finding a solution for it.

In an ideal world, I'd be able to write a Postgres function to handle this functionality. However, I'm unsure if a Postgres function can handle this type of polymorphic return in a way that would be useful to PostGraphile?

I attempted setting up https://github.com/hansololai/postgraphile-connection-filter-polymorphic as follows, without any luck either:

COMMENT ON COLUMN slugs.sluggable_type IS E'@isPolymorphic\n@polymorphicTo User\n@polymorphicTo Group';

Thank you in advance for assisting me with this question and kindly let me know if I can be of assistance with resolving it!

@benjie
Copy link
Member

benjie commented Oct 23, 2019

The problem you have is that selectGraphQLResultFromTable only works on types that have lookahead metadata generators and the union type you're using does not have any. As a rule of thumb you should only use selectGraphQLResultFromTable when the type of your field is (directly) a table or connection type that PostGraphile itself has generated.

We don't currently have an abstraction for what you're trying to achieve (support for unions in makeExtendSchemaPlugin was only added recently and hasn't been expanded to supporting query planning yet), so you have to copy/paste a far amount of boilerplate. Here's what the "row by unique constraint" resolver does:

https://github.com/graphile/graphile-engine/blob/be18000f92ddca2f9cecb7b641c6f33ee63adb0e/packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.js#L123-L153

This is basically what you need to do; in fact assuming you know what the type is going to be (which you seem to from the slugs table) you can basically use that code as-is but making sure you pick the relevant TableType and sqlFullTableName for the table the slug relates to.

So your resolver will be something along the lines of:

async resolve(_, args, resolveContext, resolveInfo) {
  const { rows: [slug] } = await pgClient.query(
    `select * from slugs where id = $1`,
    [id]
  );

  if (!slug) {
    throw new Error('Slug for Node not found');
  }

  /** The description of the table from our introspection */
  const table = pgIntrospectionResultsByKind.class.find(tbl => tbl.namespaceName = MY_POSTGRAPHILE_SCHEMA && tbl.name === slug.identifier_type);

  /** pg-sql2 representation of this table */
  const sqlFullTableName = sql.identifier(table.namespaceName, table.name);

  /** The GraphQL type associated with this table (has associated look-ahead metadata generators */
  const TableType = getTypeByName(inflection.tableType(table));

  const parsedResolveInfoFragment = parseResolveInfo(resolveInfo);
  parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin
  const resolveData = getDataFromParsedResolveInfoFragment(
    parsedResolveInfoFragment,
    TableType
  );
  const query = queryFromResolveData(
    sqlFullTableName,
    undefined,
    resolveData,
    {useAsterisk: false},
    queryBuilder => {
      queryBuilder.where(
        sql.fragment`${queryBuilder.getTableAlias()}.id = ${sql.value(slug.identifier_id)}`
      );
    },
    resolveContext,
    resolveInfo.rootValue
  );
  const { rows: [row] } = await pgClient.query(sql.compile(query));
  return row;  
}

Note much of this is boilerplate, so if you need to use this in more places I recommend making a helper to do the common parts for you.

@benjie
Copy link
Member

benjie commented Oct 23, 2019

[semi-automated message] Thanks for your question; hopefully we're well on the way to helping you solve your issue. This doesn't currently seem to be a bug in the library so I'm going to close the issue, but please feel free to keep requesting help below and if it does turn out to be a bug we can definitely re-open it 👍

You can also ask for help in the #help-and-support channel in our Discord chat.

@benjie benjie closed this as completed Oct 23, 2019
@tazsingh
Copy link
Author

Thanks @benjie, I'll give that a shot 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants