@n1ru4l/socket-io-graphql-server
Version:
[](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) [](https://www.npmjs.co
249 lines (248 loc) • 10.4 kB
JavaScript
;
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;