@graphile/pg-pubsub
Version:
Subscriptions plugin for PostGraphile using PostgreSQL's LISTEN/NOTIFY
170 lines • 8.06 kB
JavaScript
;
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