UNPKG

@graphile/pg-pubsub

Version:

Subscriptions plugin for PostGraphile using PostgreSQL's LISTEN/NOTIFY

170 lines 8.06 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const debug_1 = require("debug"); const graphql_1 = require("graphql"); const debug = (0, debug_1.default)("pg-pubsub"); const base64 = (str) => Buffer.from(String(str)).toString("base64"); const nodeIdFromDbNode = (dbNode) => base64(JSON.stringify(dbNode)); function isPubSub(pubsub) { return !!pubsub; } const withInitialValue = (initialVal, asyncIterator) => { return { [Symbol.asyncIterator]: function () { return (0, tslib_1.__asyncGenerator)(this, arguments, function* () { yield yield (0, tslib_1.__await)(initialVal); /* TODO: when we can upgrade to Node 10.3+ we can replace the below with simply: for await (const val of asyncIterator) { yield val; } */ try { while (true) { const next = yield (0, tslib_1.__await)(asyncIterator.next()); if (next.done) { return yield (0, tslib_1.__await)(next.value); } else { yield yield (0, tslib_1.__await)(next.value); } } } finally { // Terminate the previous iterator if (typeof asyncIterator.return === "function") { asyncIterator.return(); } } }); }, }; }; const PgGenericSubscriptionPlugin = function (builder, { pubsub, pgSubscriptionPrefix = "postgraphile:", pgSubscriptionAuthorizationFunction, }) { if (!isPubSub(pubsub)) { debug("Subscriptions disabled - no pubsub provided"); return; } builder.hook("inflection", (inflection, build) => build.extend(inflection, { listen() { return "listen"; }, listenPayload() { return this.upperCamelCase(`${this.listen()}-payload`); }, }), ["PgGenericSubscription"]); builder.hook("GraphQLObjectType:fields", (fields, build, graphileContext) => { const { scope: { isRootSubscription }, fieldWithHooks, } = graphileContext; const { extend, newWithHooks, pgSql: sql, graphql: { GraphQLObjectType, GraphQLNonNull, GraphQLString, GraphQLID, }, getTypeByName, $$isQuery, pgParseIdentifier: parseIdentifier, resolveNode, inflection, } = build; if (!isRootSubscription) { return fields; } const parsedFunctionParts = pgSubscriptionAuthorizationFunction ? parseIdentifier(pgSubscriptionAuthorizationFunction) : null; const Node = getTypeByName(inflection.builtin("Node")); const Query = getTypeByName(inflection.builtin("Query")); const ListenPayload = newWithHooks(GraphQLObjectType, { name: inflection.listenPayload(), fields: () => (Object.assign({ query: { description: "Our root query field type. Allows us to run any query from our subscription payload.", type: Query, resolve() { return $$isQuery; }, } }, (Node ? { relatedNode: fieldWithHooks("relatedNode", ({ getDataFromParsedResolveInfoFragment, }) => ({ type: Node, resolve: async (payload, _args, resolveContext, resolveInfo) => { if (!payload.relatedNodeId) { return null; } return resolveNode(payload.relatedNodeId, build, { getDataFromParsedResolveInfoFragment }, {}, resolveContext, resolveInfo); }, }), { isPgGenericSubscriptionPayloadRelatedNodeField: true, }), // We don't use 'nodeId' here because it's likely your cache will // use 'nodeId' as the cache key. // We need 'relatedNodeId' in addition to 'relatedNode' in case the // node itself was deleted. relatedNodeId: { type: GraphQLID, }, } : null))), }, { isPgGenericSubscriptionPayloadType: true, }); const listen = inflection.listen(); return extend(fields, { [listen]: fieldWithHooks(listen, () => ({ type: new GraphQLNonNull(ListenPayload), args: { topic: { type: new GraphQLNonNull(GraphQLString), }, initialEvent: { defaultValue: false, type: new GraphQLNonNull(graphql_1.GraphQLBoolean), description: "If true, this subscription will trigger an event as soon as it initiates.", }, }, subscribe: async (_parent, args, _context, _resolveInfo) => { const { pgClient } = _context; const topic = (pgSubscriptionPrefix || "") + args.topic; // Check they're allowed let unsubscribeTopic; if (parsedFunctionParts) { const { text, values } = sql.compile(sql.query `select unsubscribe_topic from ${sql.identifier(parsedFunctionParts.namespaceName, parsedFunctionParts.entityName)}(${sql.value(topic)}) unsubscribe_topic`); const { rows: [[_unsubscribeTopic]], } = await pgClient.query({ text, values, rowMode: "array", }); if (!_unsubscribeTopic) { throw new Error("You may not subscribe to this topic"); } unsubscribeTopic = _unsubscribeTopic; } const asyncIterator = pubsub.asyncIterator(topic); if (!asyncIterator) { return asyncIterator; } if (unsubscribeTopic) { // Subscribe to event revoking subscription const unsubscribeIterator = pubsub.asyncIterator(unsubscribeTopic); unsubscribeIterator.next().then(() => { debug("Unsubscribe triggered on channel %s", unsubscribeTopic); if (asyncIterator.return) { asyncIterator.return(); } if (unsubscribeIterator.return) { unsubscribeIterator.return(); } }); } if (args.initialEvent) { return withInitialValue(null, asyncIterator); } else { return asyncIterator; } }, resolve: async (payload, _args, _context, _resolveInfo) => { const result = Object.assign({}, payload); if (Node && payload && payload.__node__) { const nodeId = nodeIdFromDbNode(payload.__node__); result.relatedNodeId = nodeId; } return result; }, }), { isPgGenericSubscriptionRootField: true, }), }); }, ["PgGenericSubscription"], [], ["Node", "Query"]); }; exports.default = PgGenericSubscriptionPlugin; //# sourceMappingURL=PgGenericSubscriptionPlugin.js.map