UNPKG

@n1ru4l/socket-io-graphql-server

Version:

[![npm version](https://img.shields.io/npm/v/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.co

249 lines (248 loc) 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerSocketIOGraphQLServer = void 0; const graphql_1 = require("graphql"); const isAsyncIterableIterator_js_1 = require("./isAsyncIterableIterator.js"); const isDocumentNode = (input) => { return input["kind"] === "Document" && Array.isArray(input["definitions"]); }; const isSubscriptionOperation = (def) => def.operation === "subscription"; const decodeMessage = (message) => { var _a, _b, _c; let id; let operation = null; let variables = null; let operationName = null; let extensions = null; if (typeof message === "object" && message !== null) { const maybeId = message.id; if (typeof maybeId === "number") { id = maybeId; } else { return new Error("Invalid message format. Field 'id' is invalid."); } const maybeOperation = message.operation; if (typeof maybeOperation === "string" || (typeof maybeOperation === "object" && maybeOperation !== null)) { operation = maybeOperation; } else { return new Error("Invalid message format. Field 'operation' is invalid. Must be DocumentSourceString or DocumentNode."); } const maybeVariables = (_a = message.variables) !== null && _a !== void 0 ? _a : null; if (typeof maybeVariables === "object") { variables = maybeVariables; } else { return new Error("Invalid message format. Field 'variableValues' is invalid."); } const maybeOperationName = (_b = message.operationName) !== null && _b !== void 0 ? _b : null; if (maybeOperationName === null || typeof maybeOperationName === "string") { operationName = maybeOperationName; } else { return new Error("Invalid message format. Field 'operationName' is invalid."); } const maybeExtensions = (_c = message.extensions) !== null && _c !== void 0 ? _c : null; if (typeof maybeExtensions === "object") { extensions = maybeExtensions; } else { return new Error("Invalid message format. Field 'extensions' is invalid."); } return { id, operation, variables, operationName, extensions, }; } return new Error("Invalid message format. Sent message is not an object."); }; const decodeUnsubscribeMessage = (message) => { if (typeof message === "object" && message !== null) { const maybeId = message.id; if (typeof maybeId === "number") { return { id: maybeId }; } else { return new Error("Invalid message format. Field 'id' is invalid."); } } return new Error("Invalid message format. Sent message is not an object."); }; const registerSocketIOGraphQLServer = ({ socketServer, getParameter, onMessageDecodeError = console.error, isLazy = false, }) => { let acceptNewConnections = true; const disposeHandlers = new Map(); const registerSocket = (socket) => { // In case the socket is already registered :) const dispose = disposeHandlers.get(socket); if (dispose) { return dispose; } const subscriptions = new Map(); const executeHandler = async (rawMessage) => { const message = decodeMessage(rawMessage); if (message instanceof Error) { // TODO: Unify what we should do with this. onMessageDecodeError(message); return; } const emitFinalResult = (executionResult) => socket.emit("@graphql/result", { ...executionResult, id, isFinal: true, }); const { id, operation: source, variables: variableValues, operationName, extensions, } = message; const { graphQLExecutionParameter, subscribe = graphql_1.subscribe, execute = graphql_1.execute, parse = graphql_1.parse, validateSchema = graphql_1.validateSchema, validate = graphql_1.validate, validationRules = graphql_1.specifiedRules, } = await getParameter({ socket, graphQLPayload: { source, variableValues, operationName, extensions, }, }); // Validate Schema const schemaValidationErrors = validateSchema(graphQLExecutionParameter.schema); if (schemaValidationErrors.length > 0) { emitFinalResult({ errors: schemaValidationErrors }); return; } let documentAst; if (typeof source === "string") { // Parse try { documentAst = parse(source); } catch (syntaxError) { emitFinalResult({ errors: [syntaxError] }); return; } } else if (isDocumentNode(source)) { documentAst = source; } else { emitFinalResult({ errors: [ new graphql_1.GraphQLError("Invalid DocumentNode. The provided document AST node is invalid."), ], }); return; } // Validate const validationErrors = validate(graphQLExecutionParameter.schema, documentAst, validationRules); if (validationErrors.length > 0) { emitFinalResult({ errors: validationErrors, }); return; } const executionParameter = { document: documentAst, operationName, source, variableValues, ...graphQLExecutionParameter, }; const asyncIteratorHandler = async (result) => { if ((0, isAsyncIterableIterator_js_1.isAsyncIterableIterator)(result)) { subscriptions.set(id, () => { var _a; return (_a = result.return) === null || _a === void 0 ? void 0 : _a.call(result); }); for await (const subscriptionResult of result) { socket.emit("@graphql/result", { ...subscriptionResult, id }); } } else { emitFinalResult(result); } }; // TODO: change AsyncIterableIterator to AsyncGenerator once we drop support for GraphQL.js 15 let executionResult; const mainOperation = (0, graphql_1.getOperationAST)(documentAst, operationName); if (!mainOperation) { executionResult = { errors: [new graphql_1.GraphQLError("No executable operation sent.")], }; } else { try { if (isSubscriptionOperation(mainOperation)) { executionResult = await subscribe({ ...executionParameter, document: documentAst, }); } else { // TODO: remove type-cast once we drop support for GraphQL.js 15 executionResult = execute(executionParameter); } } catch (contextError) { console.error("Unexpected error occurred.", contextError); executionResult = { errors: [new graphql_1.GraphQLError("A unexpected error occurred.")], }; } } Promise.resolve(executionResult) .then((result) => { if ((0, isAsyncIterableIterator_js_1.isAsyncIterableIterator)(result)) { return asyncIteratorHandler(result); } else { emitFinalResult(result); } }) .catch((err) => { emitFinalResult({ errors: [err], }); }); }; socket.on("@graphql/execute", executeHandler); const unsubscribeHandler = (rawMessage) => { const message = decodeUnsubscribeMessage(rawMessage); if (message instanceof Error) { // TODO: Unify what we should do with this. onMessageDecodeError(message); return; } const id = message.id; const subscription = subscriptions.get(id); subscription === null || subscription === void 0 ? void 0 : subscription(); subscriptions.delete(id); }; socket.on("@graphql/unsubscribe", unsubscribeHandler); const disconnectHandler = () => { // Unsubscribe all pending GraphQL Live Queries and Subscriptions subscriptions.forEach((unsubscribe) => unsubscribe()); disposeHandlers.delete(socket); }; socket.once("disconnect", disconnectHandler); const disposeHandler = () => { socket.off("@graphql/execute", executeHandler); socket.off("@graphql/unsubscribe", unsubscribeHandler); socket.off("disconnect", disconnectHandler); disconnectHandler(); }; disposeHandlers.set(socket, disposeHandler); return disposeHandler; }; if (isLazy === false && acceptNewConnections === true) { socketServer.on("connection", registerSocket); } return { registerSocket: (socket) => { var _a; return (_a = disposeHandlers.get(socket)) !== null && _a !== void 0 ? _a : registerSocket(socket); }, disposeSocket: (socket) => { var _a; return (_a = disposeHandlers.get(socket)) === null || _a === void 0 ? void 0 : _a(); }, destroy: () => { socketServer.off("connection", registerSocket); for (const dispose of disposeHandlers.values()) { dispose(); } }, }; }; exports.registerSocketIOGraphQLServer = registerSocketIOGraphQLServer;