Skip to content

Benjie's responses megathread #153

Open

Description

@benjie

I often post useful answers in various locations (chat, issues, PRs, StackOverflow, etc). It'd be great if these answers were rewritten and inserted into the documentation for easy reference. Some might make sense in an FAQ format, some might make sense on the relevant pages of the documentation.

This thread collects some of these responses with the intention of later going through them and putting them into the docs - of course if someone wants to take on doing this that would be very welcome!

One response per message, please; each entry should:

  • have a checkbox - [ ] Added to docs to indicate completion
  • have a source URL or description (e.g. Discord, #help-and-support, 20th December 2019 @ 3:17pm UTC (please include timezone!))
  • have a copy of the original text unless it was posted on graphile GitHub in which case a link to the comment sufficies

Thanks!

Activity

  1. benjie commented on Aug 19, 2019

    @benjie
    MemberAuthor
    • Added to docs

    Source: graphile/graphile-engine#471 (comment)

    I thought hooks were just called when schema entities are created

    Correct.

    didn't really understand what the extend stuff does

    extend(a, {b: 2}) is roughly equivalent to Object.assign(a, {b: 2}) but with a tonne of extra built in protections and logging when fields conflict.

    so the plugins are ran on a builder for an object before it is actually built (and thereby can overwrite each others methods)?

    We start with an inflectors object that's something like {pluralize, singularize, camelCase}.

    Hooks get called on the result of each other to calculate the final value, so you can think of it as:

    const initialInflectors = {pluralize, singularize, camelCase}; // Supplied by graphile-build
    
    const finalInflectors =
      hook3( // Your Hook Here
        hook2( // from @graphile-contrib/pg-simplify-inflector
          hook1( // from graphile-build-pg
            initialInflectors
          )
        )
      );

    In this situation you might have that hook1 is the graphile-build-pg hook that adds all the PG-specific inflectors (graphile-build doesn't care about PG), hook2 is the simplify inflector's hook, so you need to implement hook3 to augment that with your mods.

    But who then calls these methods in the end, where is that documented?

    graphile-build (aka Graphile Engine) is responsible for managing the calling of all the hooks. They all execute synchronously. They start here:

    https://github.com/graphile/graphile-engine/blob/2c0ce76c55fe2208c1445534f1904b3844a771d2/packages/graphile-build/src/SchemaBuilder.js#L476-L493

    Especially with makeAddInflectorsPlugin I am lost, as the "inflection" hook is not documented anywhere…

    You shouldn't need to even worry about any of this, the makeAddInflectorsPlugin (documented here) should completely abstract the whole hook system so you only need to worry about writing your override:

    const { makeAddInflectorsPlugin } = require("graphile-utils");
    
    module.exports = makeAddInflectorsPlugin({
      allRows(table: PgClass) {
        return this.camelCase(
          `all-${this.distinctPluralize(this._singularizedTableName(table))}`
        );
      },
    }, true);
  2. benjie commented on Sep 13, 2019

    @benjie
    MemberAuthor

    How lookahead works/could work w.r.t. interfaces/unions: graphile/crystal#387 (comment)

  3. benjie commented on Sep 16, 2019

    @benjie
    MemberAuthor
    • Added to the docs

    graphile/crystal#387 (comment) on how the lookahead system works (and might be modified to support unions/interfaces).

  4. benjie commented on Feb 3, 2020

    @benjie
    MemberAuthor
    • Added to the docs

    thanks for making such an awesome library! We've been using it in production for over a year now. We're starting to experiment with GraphQL subscriptions, but would like to be able to add subscriptions to non-Postgres things... for example, to consume a stream of Mixpanel events from Mixpanel's API and then route those back through our GraphQL API to a web client. Do you have examples or tips of how to use Postgraphile (with Koa) to serve GraphQL subscriptions without using the LISTEN/NOTIFY pg-pubsub library? (https://www.npmjs.com/package/@graphile/pg-pubsub)

    We don't have any examples, but it's definitely possible.

    You can implement it with makeExtendSchemaPlugin; the difference is the shape of the resolvers would change:

    const resolvers = {
      TypeName: {
        fieldName: {
          subscribe(parent, args, context, info) {
            return makeYourAsyncIteratorHere();
          },
          resolve(asyncIteratorValue, args, context, info) {
            return someFunctionOf(asyncIteratorValue);
          }
        }
      }
    };

    Your async iterator can return a stream of events from anywhere.

    https://www.graphile.org/postgraphile/make-extend-schema-plugin/

    https://discordapp.com/channels/489127045289476126/498852330754801666/673926777512656916

  5. benjie commented on Feb 7, 2020

    @benjie
    MemberAuthor
    • Added to the docs

    Way to get the inflectors so you can write external code that depends on them without building the entire GraphQL schema again.

    const { getPostGraphileBuilder } = require('postgraphile-core');
    
    export async function getBuild(connectionString, schema, options) {
      const pool = new Pool({connectionString});
      try {
        const builder = await getPostGraphileBuilder(pool, schema, options);
        return builder.createBuild();
      } finally {
        pool.end();
      }
    }

    The above function gives you a way to get the build object:

    const build = await getBuild(connectionString, schema, options);

    You can use inflectors from build.inflection; they typically take introspection results as input, you can get those from build.pgIntrospectionResultsByKind. For example, to map a column name to what it would be called in GraphQL, you could do something like:

    const buildPromise = getBuild(connectionString, schema, options);
    
    async function getGraphQLColumnName(schemaName, tableName, columnName) {
      const build = await buildPromise;
      const table = build.pgIntrospectionResultsByKind.class.find(rel => rel.name === tableName && rel.namespaceName === schemaName);
      const column = build.pgIntrospectionResultsByKind.attribute.find(attr => attr.name === columnName && attr.classId === table.id);
      return build.inflection.column(attr);
    }

    https://discordapp.com/channels/489127045289476126/498852330754801666/675281797080416289

  6. benjie commented on Feb 7, 2020

    @benjie
    MemberAuthor
    • Added to docs

    Jest test to ensure RLS is enabled on all tables (from 2017).

    import { withRootDb } from '../test_helpers';
    
    test('RLS is enabled for all tables in mindtree_public', () => withRootDb(async (client) => {
      const { rows } = await client.query(`
      WITH nsp AS (
        SELECT oid FROM pg_namespace WHERE nspname = 'mindtree_public'
      )
      SELECT relname AS "table"
      FROM pg_class
      INNER JOIN nsp
      ON (nsp.oid = pg_class.relnamespace)
      WHERE relkind = 'r'
      AND relrowsecurity IS NOT DISTINCT FROM FALSE
      `);
      expect(rows).toHaveLength(0);
    }));

    graphile/crystal#461 (comment)

  7. benjie commented on Feb 13, 2020

    @benjie
    MemberAuthor
    • Added to docs

    @whollacsek:
    By the way, I've always wondered if there's a case against using simpleCollections: "only"? Currently I have it disabled but I'm starting to find tedious to always have to go through nodes when passing data down to my React components.

    @benjie:
    Basically it comes down to these questions:

    1. Do you need cursor pagination?
    2. Do you need totalCount
    3. Do you need aggregates to be performed over the collection?

    If the answer to any of these questions is "yes" or "I will" or "I might" it's probably best to be a connection.

    https://discordapp.com/channels/489127045289476126/498852330754801666/677462518419161089

  8. pinned this issue on May 1, 2020
  9. benjie commented on May 2, 2020

    @benjie
    MemberAuthor
    • Added to docs

    anxifer via Discord
    Hi. I've defined a makeExtendSchemaPlugin and want to call graphql inside of my custom mutation which is defined in the plugin. The execute command on the graphql object requires to provide a DocumentNode Element. Is there some easy possibility to access my already existing mutations / queries and to pass that info to my graphql object?

    You can indeed issue GraphQL requests from various contexts, including within a resolver. To do so you need the following:

    • Access to the graphql function from the graphql module
      • In a PostGraphile plugin, if you have access to the build object (which you usually will), you should get this from build.graphql.graphql
      • Failing that, you can import { graphql } from 'graphql' or const { graphql } = require('graphql'), but this has caveats.
    • A reference to the GraphQL schema object. You can get this from many sources:
      • in a resolver, you should extract it from info.schema
      • if you have access to the PostGraphile middleware, you can issue await postgraphileMiddleware.getGqlSchema()
      • if you don't need the PostGraphile middleware, you can use await createPostGraphileSchema(...) - see schema only usage - do this once and cache it because it's expensive to compute
    • A GraphQL operation (aka query, but includes mutations, subscriptions) to execute; this can be a string or an AST
    • The variables to feed to the operation (if necessary)
    • A valid GraphQL context for PostGraphile
      • inside a resolver, you can just pass the resolvers context straight through
      • in other situations, have a look at withPostGraphileContext in the schema only usage

    Issuing a GraphQL operation from inside a resolver example:

    /*
     * Assuming you have access to a `build` object, e.g. inside a 
     * `makeExtendSchemaPlugin`, you can extract the `graphql` function
     * from the `graphql` library here like so:
     */
    const { graphql: { graphql } } = build;
    /*
     * Failing the above: `import { graphql } from 'graphql';` but beware of
     * duplicate `graphql` modules in your `node_modules` causing issues.
     */
    
    function myResolver(parent, args, context, info) {
      // Whatever GraphQL query you wish to issue:
      const document = /* GraphQL */ `
        query MyQuery($userId: Int!) {
          userById(id: $userId) {
            username
          }
        }
      `;
      // The name of the operation in your query document (optional)
      const operationName = 'MyQuery';
      // The variables for the query
      const variables = { userId: args.userId };
    
      const { data, errors } = await graphql(
        info.schema,
        document,
        null,
        context,
        variables,
        operationName
      );
      
      return data.userById.username;
    }
  10. benjie commented on Jul 29, 2020

    @benjie
    MemberAuthor

    Added to docs Will not add to docs because this is not an approach that I like, it's a workaround of a limitation of V4 and will hopefully be addressed in V5.

    Implicitly setting the relevant companyId argument from a JWT claim on all root level query fields (and removing the argument since it's implicit).

    const myPlugin = builder => {
      builder.hook("GraphQLObjectType:fields:field", (field, _, { Self, scope }) => {
        if (!field.args || !field.args.companyId || !scope.isRootQuery) {
          return field;
        }
        // Remove the companyId field and default it from context
        const { companyId, ...remainingArgs } = field.args;
        const { resolve: oldResolve } = field;
        return {
          ...field,
          args: remainingArgs,
          resolve(parent, args, context, info) {
            return oldResolve(parent, {
              ...args,
              // Grab the relevant companyId from the JWT, or some other value
              // from additionalGraphQLContextFromRequest if you prefer
              companyId: context.jwtClaims.companyId,
            }, context, info);
          }
        };
      });
    };
  11. benjie commented on Sep 1, 2020

    @benjie
    MemberAuthor

    Wraps all resolvers returning a DateTime and automatically calls toISOString on them.

    const MyPlugin = makeWrapResolversPlugin(
      (context, build, field, options) =>
        build.graphql.getNamedType(field.type).name === "Datetime"
          ? field
          : null,
      (field) => async (resolver) => {
        const d = await resolver();
        // TODO: handle case where `field` is a list.
        return d instanceof Date ? d.toISOString() : d;
      }
    );

    -- graphile/crystal#1341 (comment)

  12. benjie commented on Sep 25, 2020

    @benjie
    MemberAuthor
    • Added to docs

    For watch mode in production you might not want to install our whole watch schema with its event triggers and what not; instead you can use the “manual” watch trigger. Then just fire that NOTIFY after all your migrations have completed.

    For this manual watch mode you still use watchPg: true, but you tell PostGraphile not to install the event listener (in production PostGraphile shouldn't have sufficient permissions to install it anyway, but explicitly stating not to removes it from your logs):

    postgraphile(DATABASE_URL, SCHEMA, {
      watchPg: true,
      graphileBuildOptions: {
        pgSkipInstallingWatchFixtures: true,
      }
    })

    Then when you're ready for PostGraphile to reload your schema, you send a {"type":"manual"} payload on the postgraphile_watch channel:

    NOTIFY "postgraphile_watch", json_build_object('type', 'manual')::text;

    -- https://discord.com/channels/489127045289476126/498852330754801666/758958528793804840

  13. benjie commented on Oct 16, 2020

    @benjie
    MemberAuthor

    IMPORTANT: doSomethingWith is not safe: (see: tc39/proposal-async-iteration#126); replace it with an async-iterator (not an async generator) native library.

    Wrapping an async iterator:

    // THIS IS NOT SAFE, DO NOT USE
    async function* doSomethingWith(asyncIterator, context) {
      console.log("Subscription started");
      try {
        for await (const val of asyncIterator) {
          yield val
        }
      } finally {
        console.log("Subscription ended");
      }
    }

    -- graphile/crystal#1363 (comment)

    e.g. as might be done when wrapping a subscribe method:

    module.exports = (builder) => {
      builder.hook('GraphQLObjectType:fields:field', (field, build, context) => {
        if (!context.scope.isRootSubscription) return field;
        const oldSubscribe = field.subscribe;
        return {
          ...field,
          async subscribe(
            rootValue,
            args,
            context /* DIFFERENT TO THE CONTEXT ABOVE */,
            info
          ) {
            const asyncIterator = await oldSubscribe(rootValue, args, context, info);
            const derivativeAsyncIterator = doSomethingWith(asyncIterator, context);
            return derivativeAsyncIterator;
          }
        }
      }, ['MySubscribeWrapperThingy'], [], ['PgSubscriptionResolver']);
    };

    -- graphile/crystal#1218 (comment)

  14. benjie commented on Oct 26, 2020

    @benjie
    MemberAuthor

    Personally I simplify things by using only one PostgreSQL role at runtime (I call this the "visitor" role, since anyone (authenticated or otherwise) who visits my site/app is a visitor) and then using RLS policies to govern what said visitor can do based on who they are. I split tables on permission boundaries, so the permissions granted (e.g. ability to update "name", "bio" field) are common to a table (e.g. "organizations") and the only decision is whether or not the current user can do those things (e.g. create policy update_admins on organizations for update using (id in (select list_of_org_ids_current_user_is_admin_of()))). Note the policy only chooses which rows can be affected; the role chooses which columns can be affected. If there are situations where you cannot split on permission boundaries and some role needs to also be able to manipulate a particular field where others can manipulate that record but not that field, then you can use a SECURITY DEFINER custom mutation function to enable operations that wouldn't normally be allowed by RBAC/RLS.

    -- https://discord.com/channels/489127045289476126/498852330754801666/770223393499774996

  15. benjie commented on Dec 24, 2020

    @benjie
    MemberAuthor

    Here's an example of a plugin that adds a condition to a PostGraphile collection by default: https://github.com/graphile-contrib/pg-omit-archived/blob/6ce933efc9e83bbf9415d3ab1393326111f84b42/index.js#L194-L255

    The important part is the addArgDataGenerator @ https://github.com/graphile-contrib/pg-omit-archived/blob/6ce933efc9e83bbf9415d3ab1393326111f84b42/index.js#L234

    Stripped down it'd be something like:

    const MyFilterPlugin = builder => {
      builder.hook(
        "GraphQLObjectType:fields:field:args",
        (args, build, context) => {
          const { pgSql: sql } = build;
          const { addArgDataGenerator } = context;
          if (...it's not relevant to us...) {
            return args;
          }
          addArgDataGenerator(fieldArgs => ({
            pgQuery(queryBuilder) {
              queryBuilder.where(sql.fragment`my_condition = here`);
            },
          }));
    
          return args;
        },
      );
    };

    The main parts you'd need to implement are a) finding out which fields are relevant to you (e.g. if pgFieldIntrospection relates to the table you care about), and b) adding the condition into the sql.fragment

    If you just want to apply it to a specific field Foo.bar (the bar field on the Foo type) then the answer to (a) might be something like context.Self.name === 'Foo' && context.field.name === 'bar'

    If not relevant; return args early.

  16. benjie commented on Jan 13, 2021

    @benjie
    MemberAuthor

    A simple server (microservice) to enqueue jobs to Graphile Worker; assertRequestIsAuthenticated is left as an exercise to the reader:

    const { makeWorkerUtils } = require("graphile-worker");
    const express = require("express");
    const bodyParser = require("body-parser");
    
    const app = express();
    app.use(bodyParser.json());
    
    async function main() {
      const workerUtils = await makeWorkerUtils({
        connectionString: "postgres:///my_db",
      });
      try {
        await workerUtils.migrate();
        app.post('/add_job', async (req, res, next) => {
          try {
            assertRequestIsAuthenticated(req);
            await workerUtils.addJob(req.body.task, req.body.payload, /* etc */);
          } catch (e) {
            next(e);
          }
        });
        app.listen(3030);
      } finally {
        await workerUtils.release();
      }
    }
    
    main().catch((err) => {
      console.error(err);
      process.exit(1);
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions