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

Benjie's responses megathread #153

Open
benjie opened this issue Aug 19, 2019 · 15 comments
Open

Benjie's responses megathread #153

benjie opened this issue Aug 19, 2019 · 15 comments

Comments

@benjie
Copy link
Member

benjie commented Aug 19, 2019

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!

@benjie
Copy link
Member Author

benjie commented Aug 19, 2019

  • 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);

@benjie
Copy link
Member Author

benjie commented Sep 13, 2019

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

@benjie
Copy link
Member Author

benjie commented Sep 16, 2019

  • Added to the docs

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

@benjie
Copy link
Member Author

benjie commented Feb 3, 2020

  • 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

@benjie
Copy link
Member Author

benjie commented Feb 7, 2020

  • 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

@benjie
Copy link
Member Author

benjie commented Feb 7, 2020

  • 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)

@benjie
Copy link
Member Author

benjie commented Feb 13, 2020

  • 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

@benjie benjie pinned this issue May 1, 2020
@benjie
Copy link
Member Author

benjie commented May 2, 2020

  • 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;
}

onpaws pushed a commit to onpaws/graphile.github.io that referenced this issue Jul 7, 2020
onpaws pushed a commit to onpaws/graphile.github.io that referenced this issue Jul 7, 2020
@benjie
Copy link
Member Author

benjie commented Jul 29, 2020

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);
      }
    };
  });
};

@benjie
Copy link
Member Author

benjie commented Sep 1, 2020

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)

@benjie
Copy link
Member Author

benjie commented Sep 25, 2020

  • 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

@benjie
Copy link
Member Author

benjie commented Oct 16, 2020

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)

@benjie
Copy link
Member Author

benjie commented Oct 26, 2020

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

@benjie
Copy link
Member Author

benjie commented Dec 24, 2020

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.

@benjie
Copy link
Member Author

benjie commented Jan 13, 2021

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
@benjie and others