Coder Social home page Coder Social logo

operation-hooks's People

Contributors

benjie avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

jcfinnerup kzlar

operation-hooks's Issues

SQL operation hooks should be per-table

Otherwise the risk of omitting one is too high. Custom mutations and extensions can handle their own pre/post events, so they don't really need it.

This will be a breaking change.

Question about the format of callback's 'input' argument

I'm using the operation hooks plugin and I'm confused about the format of the input field of the callback.
This is what it looks like for my query:

{    
    data: [
      { '@nodes': { __identifiers: [1], username: 'alice' } },
      { '@nodes': { __identifiers: [2], username: 'bob' } }
    ],
    startCursor: null,
    endCursor: null
}

And this is the result of the same query that I get on the frontend:

{
    users: {
      nodes: [
        { username: 'alice', __typename: 'User' },
        { username: 'bob', __typename: 'User' }
      ],
      __typename: 'UsersConnection'
    }
}

I'm trying to get the input in the callback into the latter format, but have no idea where to start. What is the best way to go about this?

Logic error: operation hook returned 'undefined' with Byid queries

We are getting an error "Logic error: operation hook returned 'undefined'." when no record found with any "*ById" query on any table.

This error seems to be coming from "graphql/oprational-hooks".

Environment to reproduce the issue:

  • run the postgraphile with the following hooks:
    makePluginHook([OperationHooks, PgPubsub, subscriptionsLds]);
  • execute *ById query on any table passing non existing record *id to reproduce the error

Example to reproduce:

Table:

CREATE TABLE account ( "id" serial primary key, "firstName" text, "lastName" text, "dateOfBirth" date, "createdAt" timestamptz not null default now(), "updatedAt" timestamptz not null default now() )

Sample Request:

Note: Any non existing record id, example id = 10, a non existing record.

{ accountById(id: 10) { id } }

Sample Response:

{ "errors": [ { "message": "Logic error: operation hook returned 'undefined'.", "locations": [ { "line": 2, "column": 3 } ], "path": [ "accountById" ], "extensions": { "messages": [] } } ], "data": { "accountById": null } }

@benjie, Please let me know if you need any further information to reproduce the issue.

Access RAISE NOTICE details from GraphQL client

Feature description

I've been using #16 to add out of band messages to my GraphQL mutations and it has been a blast! In my usage I employ detail = json_build_object(...) to add auxiliary data to my messages and I am able to access them in my operation hooks on the server.

However, while I'm able to access the messages part on my GraphQL client, I'm unable to access the detail part since it's not a part of OperationMessageInterface in the GraphQL schema.

Motivating example

Really any time you'd use detail as part of your notice and want to add auxiliary data. e.g. expanding on the minimal example in the README:

RAISE NOTICE 'Your credits are running low.' USING ERRCODE = 'OPMSG', detail = json_build_object('remaining_credits',5);

Breaking changes

This is only expanding the GraphQL schema, don't believe anything is breaking.

Supporting development

  • am interested in building this feature myself
    (would tick this if I thought I was capable of doing it myself 😅 )
  • am interested in collaborating on building this feature
  • am willing to help testing this feature before it's released
  • am willing to write a test-driven test suite for this feature (before it exists)
  • am a Graphile sponsor ❤️
  • have an active support or consultancy contract with Graphile

failed assertion at PgNoticeMessagesPlugin: "processNotice should not be set yet!"

Summary

I have come across an assertion error message when using operation-hooks in combination with a different plugin "postgraphile-plugin-many-create-update-delete" (hereafter "mn-plugin"). Both plugins appear to be working perfectly when not used together and the error is thrown when attempting to make a mutation pursuant to the mn-plugin; however the error is thrown by an assertion of the operation-hook plugin. Also, if I comment out the assertion located at line 39 of the source file "PgNoticeMessagesPlugin" that appears to resolve the conflict; however, I do not know enough about the plugin system to understand the basis for the assertion.

Steps to reproduce

  "dependencies": {
    "@graphile/operation-hooks": "^1.0.0",
    "postgraphile-plugin-many-create-update-delete": "^1.0.6"
  },

After standard pg-library setup, preform a mn-plugin mutation:

mutation {
  mnCreateIndividual(
    input: {
      mnIndividual: [
        { first: "John", last: "Doe" }
        { first: "Jane", last: "Doe" }
      ]
    }
  ) {
    query {
      individuals {
        nodes {
          first
          last
        }
      }
    }
  }
}

Expected results

This is the correct result that I get when commenting out the assertion located at line 39 of the source file "PgNoticeMessagesPlugin":

{
  "data": {
    "mnCreateIndividual": {
      "query": {
        "individuals": {
          "nodes": [
            {
              "first": "John",
              "last": "Doe"
            },
            {
              "first": "Jane",
              "last": "Doe"
            }
          ]
        }
      }
    }
  },
  "explain": [
    {
      "query": "with __local_0__ as (\n          INSERT INTO \"public\".\"individuals\" \n          (\"individual_id\", \"tenant_id\", \"first\", \"last\", \"prefix\", \"middle\", \"gender\", \"suffix\", \"ssn\", \"birth\", \"death\")\n            VALUES (default, default, $1, $2, default, default, default, default, default, default, default),(default, default, $3, $4, default, default, default, default, default, default, default) returning *) select ((case when __local_0__ is null then null else __local_0__ end))::text from __local_0__",
      "plan": "CTE Scan on __local_0__  (cost=0.03..0.08 rows=2 width=32)\n  CTE __local_0__\n    ->  Insert on individuals  (cost=0.00..0.03 rows=2 width=1480)\n          ->  Values Scan on \"*VALUES*\"  (cost=0.00..0.03 rows=2 width=1480)"
    },
    {
      "query": "with __local_0__ as (select (str::\"public\".\"individuals\").*\nfrom unnest(($1)::text[]) str) select to_json(__local_0__.\"individual_id\") as \"@ophookpk__individual_id\"\nfrom __local_0__ as __local_0__\n\nwhere (TRUE) and (TRUE)\n\n\n",
      "plan": "Function Scan on unnest str  (cost=0.00..0.04 rows=2 width=32)"
    },
    {
      "query": "with __local_0__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_1__.\"individual_id\"), 'first'::text, (__local_1__.\"first\"), 'last'::text, (__local_1__.\"last\")))) as \"@nodes\" from (select __local_1__.*\nfrom \"public\".\"individuals\" as __local_1__\n\nwhere (TRUE) and (TRUE)\norder by __local_1__.\"individual_id\" ASC\n\n) __local_1__), __local_2__ as (select json_agg(to_json(__local_0__)) as data from __local_0__) select coalesce((select __local_2__.data from __local_2__), '[]'::json) as \"data\" ",
      "plan": "Result  (cost=13.18..13.19 rows=1 width=32)\n  InitPlan 1 (returns $0)\n    ->  Aggregate  (cost=13.16..13.17 rows=1 width=32)\n          ->  Sort  (cost=11.91..12.04 rows=50 width=1480)\n                Sort Key: __local_1__.individual_id\n                ->  Seq Scan on individuals __local_1__  (cost=0.00..10.50 rows=50 width=1480)"
    }
  ]
}

Actual results

The exact error message as it appears in graphiql is:

{
  "errors": [
    {
      "errcode": "ERR_ASSERTION",
      "extensions": {
        "messages": [],
        "exception": {
          "errcode": "ERR_ASSERTION"
        }
      },
      "message": "processNotice should not be set yet!",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "mnCreateIndividual"
      ],
      "stack": [
        "AssertionError [ERR_ASSERTION]: processNotice should not be set yet!",
        "    at registerNotifyListener (/Users/richard/repos/yp-postgraphile/node_modules/@graphile/operation-hooks/lib/PgNoticeMessagesPlugin.js:39:17)",
        "    at applyHooks (/Users/richard/repos/yp-postgraphile/node_modules/@graphile/operation-hooks/lib/OperationHooksCorePlugin.js:9:28)",
        "    at processTicksAndRejections (internal/process/task_queues.js:95:5)",
        "    at async resolve (/Users/richard/repos/yp-postgraphile/node_modules/@graphile/operation-hooks/lib/OperationHooksCorePlugin.js:116:38)",
        "    at async resolve (/Users/richard/repos/yp-postgraphile/node_modules/@graphile/operation-hooks/lib/OperationHooksCorePlugin.js:122:32)",
        "    at async /Users/richard/repos/yp-postgraphile/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:165:24",
        "    at async withAuthenticatedPgClient (/Users/richard/repos/yp-postgraphile/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:105:24)",
        "    at async /Users/richard/repos/yp-postgraphile/node_modules/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:708:34",
        "    at async Promise.all (index 0)",
        "    at async graphqlRouteHandler (/Users/richard/repos/yp-postgraphile/node_modules/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:640:23)"
      ]
    }
  ],
  "data": {
    "mnCreateIndividual": null
  },
  "explain": []
}

Additional context

I don't believe my setup is relevant, but here it is:

macOS 11.6
node v14.17.6 
"postgraphile": "^4.12.3"

Possible Solution

Someone who understands the purpose of the assertion should evaluate why this other plugin is causing a failure and then decide whether the test should be changed, or if one of the plugins has an issue that needs to be addressed.

Getting error "Default resolvers at the root level are not supported" following the custom subscriptions guide

Hello! I'm following the guide for custom subscriptions here: https://www.graphile.org/postgraphile/subscriptions/

Which results in the following error for me on server start: Error: Default resolver found for field Subscription.communityFeedItemCommentAdded; default resolvers at the root level are not supported by operation-hooks…

Here is my plugin:

const topicFromArgs = (args) => {
  return `graphql:communityFeedItemComments:${args.communityFeedItemId}`;
};

makeExtendSchemaPlugin(
  ({ pgSql: sql }) => ({
    typeDefs: gql`
      type CommunityFeedItemCommentSubscriptionPayload {
        communityFeedItemComment: CommunityFeedItemComment
        event: String
      }
      extend type Subscription {
        communityFeedItemCommentAdded(communityFeedItemId: Int!): CommunityFeedItemCommentSubscriptionPayload @pgSubscription(topic: ${embed(topicFromArgs)})
      }
    `,
    resolvers: {
      CommunityFeedItemCommentSubscriptionPayload: {
        async communityFeedItemComment(...args) {
          return require('./resolver').default(sql, ...args); // used to hot reload code in dev
        },
      },
    },
  }),
)
// resolver.js
export default async (
  sql,
  event,
  _args,
  _context,
  { graphile: { selectGraphQLResultFromTable } },
) => {
  const rows = await selectGraphQLResultFromTable(
    sql.fragment`public.community_feed_item_comment`,
    (tableAlias, sqlBuilder) => {
      sqlBuilder.where(
        sql.fragment`${tableAlias}.id = ${sql.value(event.subject)}`,
      );
    },
  );
  return {
    communityFeedItemComment: rows[0],
    event,
  };
};

If I explicitly create a resolver for communityFeedItemCommentAdded, then I think selectGraphQLResultFromTable doesn't look ahead nor does it collect the fields. Thus leading back to the above error.

Any ideas about how to resolve? Thanks!

error when requiring library

On v0.2.2, require('@graphile/operation-hooks') will throw:
Cannot find module ./node_modules/@graphile/operation-hooks/lib/index.js. Please verify that the package.json has a valid "main" entry

Allow messages to be registered from any SQL function

One of the great things about this plugin is that it allows pre/post mutation functions to "register" one or more notification messages that can be passed back to the GraphQL client. Crucially, these messages augment (not replace) the mutation response, which means that the mutation can still return what the client expects (e.g. the new entity in the case of an insert) PLUS any optional relevant notification messages.

It would be great if this convenience could be extended to ALL mutation functions. In particular, I'd like for any database function to be able to register one or more notification messages to an "ambient" transaction-specific context that could ultimately be read by PostGraphile (at the end of the request/response lifecycle, when generating the GraphQL response) to add those messages to the response, just like it currently does with the operation hooks functions.

Messages could be added via a universally accessible convenience function accepting the same four properties of the current notification type. For example, add_notification(type, message, code, path) would add a notification message to the "ambient" transaction-specific context.

Ideally, messages could also be checked via similar convenience functions, so that database functions may check if messages have been added by other functions. For example get_notifications() or get_notification(type) or even has_notification_error() could be used by a function to check if another called function added a notification of type error that should cause the transaction to abort or short-circuit.

Of course, there is the question of what an "ambient" transaction-specific context is, and how it would be implemented. By "ambient" I mean a context that can be accessed directly and from any function, that is, without having to pass it explicitly from function to function. In this sense, it would be similar to any global variable or settings, like the ones used to store and access the JWT claims. By transaction-specific, I mean that the context would apply to the currently running transaction only, with functions not having to worry about any transactional concerns.

As for how to implement it, there are several possibilities, but I'll leave that to the experts (I'm looking at you Benjie ;)

Irrespective of how it's implemented, an universal mechanism to register notifications from any function would be invaluable for complex applications that need to perform non-trivial business logic. It would allow functions to keep their signatures intact (no need to pass notifications messages back and forth explicitly) while still having the ability to return any relevant notifications to the GraphQL client.

Here's some potential implementations @benjie / @demianuco discussed:

Sending messages with LISTEN/NOTIFY

One possibility we've discussed is using PostgreSQL Listen/Notify, in which the add_notification() function would notify in a channel previously created (and subscribed to) by PostGraphile, which would "accumulate" notifications thus and ultimately append them to the GraphQL response at the end of the response life-cycle. There are two problems with this approach, however, one being that the payload is limited (shouldn't be a problem, as notification messages should be small), and the other being that the mechanism will not survive a transaction rollback, thus making it impossible to send errors back to the client (show stopper!).

Sending messages with RAISE NOTICE

Another possibility is to using the simpler RAISE NOTICE USING ERRCODE statement, which raises an indelible notice that is unaffected by rollbacks, and which PostGraphile can used to accumulate notification messages in a more prosaic way. It is a bit of a hack, in the sense that RAISE NOTICE was not meant for this, and the notification might pollute logs (though this could also be an advantage for development and debugging!). Also, it is perhaps fragile as it relies on the right level of verbosity being configured, with any changes to that verbosity breaking the functionality.

Sending messages via unlogged table

Another possibility is to use an explicit global table (e.g. tx_notifications) to which notification messages would be added. It would obviously have to be transactionally scoped, with a column storing the current transaction ID (perhaps select txid_current()?), but with the transactional concerns encapsulated away by the convenience functions, so that client functions could add notification messages without having to know about transactions. The table would have no limitations in terms of payload, but I'm concerned about performance and contention issues, especially because this table would be highly volatile and in constant and concurrent use. Perhaps the use of an unlogged table is appropriate in this scenario.

Achive cookie with the help of graphile/operation-hooks

Use case:

We have 2 Postgraphile, 1 for anonymous and another for authorized access.
Keycloak used is used as IAM. So a user created at keycloak can generate access_token and refresh_token.
For security purpose these tokens can`t be holded in UI. In anonymous there is a function authenticateUser, i tired to use graphile/operation-hooks to achieve this.

here are my codes.

server.js


const express = require("express");
const { postgraphile, makePluginHook } = require("postgraphile");
const app = express();
const cors = require('cors');
const cookieParser = require('cookie-parser');
const { OperationHooksPlugin } = require("@graphile/operation-hooks");
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(function(req, res, next) {
    next();
});
const additionalGraphQLContextFromRequest= (req, res) => {
    return {
         setCookie: function(access_token) {  res.cookie("access_token", 'Bearer ' + access_token,
          {                           
                 httpOnly: true,
                 SameSite: true,
               });
            },
         setRefreshCookie: function(refresh_token) {  res.cookie("refresh_token",   refresh_token)}       
    };
};

app.use(
    postgraphile(
        process.env.DATABASE_URL,
        process.env.SCHEMA.split(','),
        {                       
            additionalGraphQLContextFromRequest,           
            jwtSecret: "ecurewebtoken",
            jwtPgTypeIdentifier: "dcp_lib.jwt_token_postgraphile",
            watchPg: true,
            retryOnInitFail: true,
            graphiql: true,
            enhanceGraphiql: true,
            enableCors: false,
            disableWarning: true,
            operationMessages: true,
            operationMessagesPreflight: true,
            appendPlugins: [OperationHooksPlugin,require("@graphile-contrib/pg-simplify-inflector"), require("postgraphile-plugin-connection-filter"), require('./set-auth-cookie.js')],
            bodySizeLimit: "4MB"

        }
    )
);

app.listen(process.env.PORT);

set-auth-cookie.js


const useAuthCredentials = (build) => (fieldContext) => {
    const {
      scope: { isRootMutation, isPgCreateMutationField, pgFieldIntrospection }
    } = fieldContext;  
    if(!pgFieldIntrospection ||  
      pgFieldIntrospection.name !== "authenticateUser") {  
        return null;  
    }
  return {
    after: [
      {
        priority: 1000,
        callback: (result, args, context) => {        
          context.setRefreshCookie(JSON.parse(result.data).refresh_token);
          context.setCookie((JSON.parse(result.data).access_token));
        }
      }
    ]
  };
}
module.exports = function MyOperationHookPlugin(builder) {
  builder.hook("init", (_, build) => {
    build.addOperationHook(useAuthCredentials(build));
    return _;
  });
};

The error getting while calling authenticateUser

{
  "errors": [
    {
      "message": "Logic error: operation hook returned 'undefined'.",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": [
        "authenticateUser"
      ],
      "extensions": {
        "messages": []
      }
    }
  ],
  "data": {
    "authenticateUser": null
  }
}

if there is any errors with my coding, please help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.