graphile / operation-hooks Goto Github PK
View Code? Open in Web Editor NEWRegister asynchronous callbacks before/after operations; uses include: validation, authorization, notification
Register asynchronous callbacks before/after operations; uses include: validation, authorization, notification
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.
Invocation of SQL functions in PgOperationHooksPlugin line 85 should cast the input param to JSONB instead of simply JSON (as per README documentation).
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?
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:
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.
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.
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);
This is only expanding the GraphQL schema, don't believe anything is breaking.
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.
"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
}
}
}
}
}
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)"
}
]
}
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": []
}
I don't believe my setup is relevant, but here it is:
macOS 11.6
node v14.17.6
"postgraphile": "^4.12.3"
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.
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!
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
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:
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!).
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.
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.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.