From 47e736924933d78185376a999ff7728e470109c8 Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 09:50:43 -0600 Subject: [PATCH 01/10] feat(api): add GraphQL endpoint * Introduced a new GraphQL API endpoint for flexible data querying * Enabled the GraphiQL playground interface in non-production environments --- server/app.js | 14 ++ server/package-lock.json | 337 +++++++++++++++++++++++++++++++++++++-- server/package.json | 7 +- 3 files changed, 345 insertions(+), 13 deletions(-) diff --git a/server/app.js b/server/app.js index af02a93..9075cdc 100644 --- a/server/app.js +++ b/server/app.js @@ -10,6 +10,8 @@ const mongoose = require('mongoose'); const http = require('http'); +const { ruruHTML } = require('ruru/server'); + const config = require('./config'); // Middleware: Bodyparser @@ -30,6 +32,18 @@ const routes = glob.sync(`${config.root}/routers/*.js`); routes.forEach((router) => { app.use('/data', require(router)); }); +// Graphql +app.all('/graphql', require('./graphql/graphqlHandler')); +if (config.env !== 'production') { + // GraphiQL interface + app.get('/graphiql', (req, res) => { + const html = ruruHTML({ + endpoint: '/graphql', + title: 'MARRVEL GraphiQL Interface', + }); + res.send(html); + }); +} // Check liftOver Configuration diff --git a/server/package-lock.json b/server/package-lock.json index a3054a3..3e2a810 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,15 +12,18 @@ "bluebird": "^3.7.2", "body-parser": "^1.18.3", "circular-json": "^0.5.9", - "express": "^4.16.4", + "express": "^4.21.2", "glob": "^7.1.7", "got": "^11.8.3", + "graphql": "^16.12.0", + "graphql-http": "^1.22.4", "moment": "^2.30.1", "mongoose": "^5.13.23", "morgan": "^1.10.0", "nunjucks": "^3.2.3", "request": "^2.88.2", - "request-promise": "^4.2.6" + "request-promise": "^4.2.6", + "ruru": "^2.0.0-rc.1" }, "devDependencies": { "@eslint/js": "^9.30.0", @@ -28,6 +31,21 @@ "nodemon": "^1.18.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -664,6 +682,15 @@ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, + "node_modules/@types/interpret": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/interpret/-/interpret-1.1.3.tgz", + "integrity": "sha512-uBaBhj/BhilG58r64mtDb/BEdH51HIQLgP5bmWzc5qCtFMja8dCk/IOJmk36j0lbi9QHwI6sbtUNGuqXdKCAtQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -691,9 +718,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "22.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -708,6 +735,12 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "license": "MIT" + }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -823,11 +856,19 @@ "node": ">=4" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1423,7 +1464,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1543,6 +1583,20 @@ "node": ">=0.10.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -1573,7 +1627,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1586,7 +1639,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1933,6 +1985,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1981,6 +2039,15 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2201,6 +2268,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -2680,6 +2753,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2855,6 +2937,85 @@ "dev": true, "license": "ISC" }, + "node_modules/graphile-config": { + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/graphile-config/-/graphile-config-1.0.0-rc.1.tgz", + "integrity": "sha512-s7e+gWPJ+S+vI1E7A3H9ku/CswGNL3UQ6qvdbfukGNHVPhg+LvZyojSX1zjpvdMYQ9mNxJMwL+7J4F2VtRhTKg==", + "license": "MIT", + "dependencies": { + "@types/interpret": "^1.1.3", + "@types/node": "^22.16.3", + "@types/semver": "^7.7.0", + "chalk": "^4.1.2", + "debug": "^4.4.1", + "interpret": "^3.1.1", + "semver": "^7.7.2", + "tslib": "^2.8.1", + "yargs": "^17.7.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/graphile-config/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/graphile-config/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/graphile-config/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-http": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.4.tgz", + "integrity": "sha512-OC3ucK988teMf+Ak/O+ZJ0N2ukcgrEurypp8ePyJFWq83VzwRAmHxxr+XxrMpxO/FIwI4a7m/Fzv3tWGJv0wPA==", + "license": "MIT", + "workspaces": [ + "implementations/**/*" + ], + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -2882,7 +3043,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3002,6 +3162,20 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -3120,6 +3294,15 @@ "dev": true, "license": "ISC" }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3223,6 +3406,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5292,6 +5484,21 @@ "node": ">=4" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -5397,6 +5604,30 @@ "node": ">=16" } }, + "node_modules/ruru": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/ruru/-/ruru-2.0.0-rc.1.tgz", + "integrity": "sha512-QaQqpYs2daSWxUO4fsY10noCkXeqbx2qiR5lWRnoScdy6mD/0ehSkxKBZhc83WyFaVA3rVurs8/Dd/iIn0wbXw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "^1.3.1", + "graphile-config": "^1.0.0-rc.1", + "graphql": "^16.9.0", + "http-proxy": "^1.18.1", + "tslib": "^2.8.1", + "yargs": "^17.7.2" + }, + "bin": { + "ruru": "dist/cli-run.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphile-config": "^1.0.0-rc.1", + "graphql": "^16.9.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5929,6 +6160,32 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -5956,7 +6213,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6090,6 +6346,12 @@ "node": ">=0.8" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6547,6 +6809,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6575,6 +6854,42 @@ "node": ">=4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/server/package.json b/server/package.json index 89cd694..9f6c237 100644 --- a/server/package.json +++ b/server/package.json @@ -13,15 +13,18 @@ "bluebird": "^3.7.2", "body-parser": "^1.18.3", "circular-json": "^0.5.9", - "express": "^4.16.4", + "express": "^4.21.2", "glob": "^7.1.7", "got": "^11.8.3", + "graphql": "^16.12.0", + "graphql-http": "^1.22.4", "moment": "^2.30.1", "mongoose": "^5.13.23", "morgan": "^1.10.0", "nunjucks": "^3.2.3", "request": "^2.88.2", - "request-promise": "^4.2.6" + "request-promise": "^4.2.6", + "ruru": "^2.0.0-rc.1" }, "devDependencies": { "@eslint/js": "^9.30.0", From fe3aa5d241a8a708877bd96bd664a1bc13e61d1d Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 09:52:58 -0600 Subject: [PATCH 02/10] feat(graphql): add ClinVar schema and resolver Added ClinVar schema and corresponding resolver to the GraphQL API, enabling ClinVar data querying through the GraphQL endpoint. --- server/graphql/graphqlHandler.js | 40 ++++++++++++ server/graphql/index.js | 17 ++++++ server/graphql/resolvers/clinvar.resolvers.js | 61 +++++++++++++++++++ server/graphql/schemas/clinvar.schema.graphql | 40 ++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 server/graphql/graphqlHandler.js create mode 100644 server/graphql/index.js create mode 100644 server/graphql/resolvers/clinvar.resolvers.js create mode 100644 server/graphql/schemas/clinvar.schema.graphql diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js new file mode 100644 index 0000000..3a1fc50 --- /dev/null +++ b/server/graphql/graphqlHandler.js @@ -0,0 +1,40 @@ +const { createHandler } = require('graphql-http/lib/use/express'); +const { buildSchema } = require('graphql'); +const { readFileSync } = require('fs'); +const path = require('path'); + +// Import resolvers +const clinvarResolvers = require('./resolvers/clinvar.resolvers'); + +// Read schema files +const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); + +// Combine all schemas +const typeDefs = ` + ${clinvarTypeDefs} + + type Query { + clinvarByGeneSymbol(symbol: String!): [Clinvar!]! + clinvarByGeneEntrezId(entrezId: Int!): [Clinvar!]! + clinvarByVariant(variant: String!, build: String = "hg19"): Clinvar + clinvarCountsByEntrezId(entrezId: Int!): ClinvarCounts! + } +`; + +// Build the schema +const schema = buildSchema(typeDefs); + +// Create root resolver +const rootValue = { + clinvarByGeneSymbol: clinvarResolvers.findByGeneSymbol, + clinvarByGeneEntrezId: clinvarResolvers.findByGeneEntrezId, + clinvarByVariant: clinvarResolvers.findByVariant, + clinvarCountsByEntrezId: clinvarResolvers.getCountsByEntrezId, +}; + +// Create and export the GraphQL handler +module.exports = createHandler({ + schema, + rootValue, + graphiql: process.env.NODE_ENV !== 'production', // Enable GraphiQL in development +}); diff --git a/server/graphql/index.js b/server/graphql/index.js new file mode 100644 index 0000000..1f1259d --- /dev/null +++ b/server/graphql/index.js @@ -0,0 +1,17 @@ +/** + * GraphQL API module for MARRVEL + * + * This module provides GraphQL API endpoints for various data sources + * including Clinvar, genes, DIOPT, and other genomic databases. + * + * Usage: + * - Access GraphQL playground at /graphql (in development) + * - Send GraphQL queries to /graphql endpoint + * + * Available queries: + * - Clinvar: findByGeneSymbol, findByGeneEntrezId, findByVariant, getCountsByEntrezId + */ + +module.exports = { + handler: require('./graphqlHandler'), +}; diff --git a/server/graphql/resolvers/clinvar.resolvers.js b/server/graphql/resolvers/clinvar.resolvers.js new file mode 100644 index 0000000..df731c1 --- /dev/null +++ b/server/graphql/resolvers/clinvar.resolvers.js @@ -0,0 +1,61 @@ +const db = require('../../utils/db'); +const utils = require('../../utils'); + +/** + * Find Clinvar variants by gene symbol + */ +const findByGeneSymbol = async ({ symbol }) => { + try { + return await db.clinvar.getByGeneSymbol(symbol); + } catch (error) { + console.error('Error fetching Clinvar by gene symbol:', error); + throw new Error('Failed to fetch Clinvar data by gene symbol'); + } +}; + +/** + * Find Clinvar variants by gene Entrez ID + */ +const findByGeneEntrezId = async ({ entrezId }) => { + try { + return await db.clinvar.getByGeneEntrezId(entrezId); + } catch (error) { + console.error('Error fetching Clinvar by gene Entrez ID:', error); + throw new Error('Failed to fetch Clinvar data by gene Entrez ID'); + } +}; + +/** + * Find Clinvar variant by specific variant + */ +const findByVariant = async ({ variant, build = 'hg19' }) => { + try { + const parsedVariant = utils.variant.validateAndParseVariant(variant); + if (!parsedVariant) { + throw new Error('Invalid variant format'); + } + return await db.clinvar.getByVariant(parsedVariant, build); + } catch (error) { + console.error('Error fetching Clinvar by variant:', error); + throw new Error('Failed to fetch Clinvar data by variant'); + } +}; + +/** + * Get Clinvar variant counts by gene Entrez ID + */ +const getCountsByEntrezId = async ({ entrezId }) => { + try { + return await db.clinvar.getCountsByEntrezId(entrezId); + } catch (error) { + console.error('Error fetching Clinvar counts by Entrez ID:', error); + throw new Error('Failed to fetch Clinvar counts by Entrez ID'); + } +}; + +module.exports = { + findByGeneSymbol, + findByGeneEntrezId, + findByVariant, + getCountsByEntrezId, +}; diff --git a/server/graphql/schemas/clinvar.schema.graphql b/server/graphql/schemas/clinvar.schema.graphql new file mode 100644 index 0000000..54b87d2 --- /dev/null +++ b/server/graphql/schemas/clinvar.schema.graphql @@ -0,0 +1,40 @@ +""" +Significance information for a Clinvar variant +""" +type ClinvarSignificance { + description: String + lastEvaluated: String + reviewStatus: String +} + +""" +A Clinvar variant entry +""" +type Clinvar { + chr: String! + start: Int! + stop: Int! + ref: String + alt: String + uid: Int + condition: String + title: String + significance: ClinvarSignificance + band: String + interpretation: String + grch38Chr: String + grch38Start: Int + grch38Stop: Int + grch38Ref: String + grch38Alt: String +} + +""" +Summary counts of Clinvar variants by significance +""" +type ClinvarCounts { + pathogenic: Int! + likelyPathogenic: Int! + likelyBenign: Int! + benign: Int! +} \ No newline at end of file From fc62c75d2e080454c38bd1d58a133d57386dd5e3 Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 09:55:46 -0600 Subject: [PATCH 03/10] feat(graphql): add Gene schema and resolver Added Gene schema and resolver to the GraphQL API, enabling gene-level queries. --- server/graphql/graphqlHandler.js | 17 +++ server/graphql/index.js | 2 + server/graphql/resolvers/gene.resolvers.js | 118 +++++++++++++++++++++ server/graphql/schemas/gene.schema.graphql | 31 ++++++ 4 files changed, 168 insertions(+) create mode 100644 server/graphql/resolvers/gene.resolvers.js create mode 100644 server/graphql/schemas/gene.schema.graphql diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index 3a1fc50..d8c8a6f 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -5,19 +5,29 @@ const path = require('path'); // Import resolvers const clinvarResolvers = require('./resolvers/clinvar.resolvers'); +const geneResolvers = require('./resolvers/gene.resolvers'); // Read schema files const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); +const geneTypeDefs = readFileSync(path.join(__dirname, 'schemas/gene.schema.graphql'), 'utf8'); // Combine all schemas const typeDefs = ` ${clinvarTypeDefs} + ${geneTypeDefs} type Query { clinvarByGeneSymbol(symbol: String!): [Clinvar!]! clinvarByGeneEntrezId(entrezId: Int!): [Clinvar!]! clinvarByVariant(variant: String!, build: String = "hg19"): Clinvar clinvarCountsByEntrezId(entrezId: Int!): ClinvarCounts! + + geneBySymbol(symbol: String!, taxonId: Int!): Gene + geneByEntrezId(entrezId: Int!): Gene + geneByHgncId(hgncId: Int!): Gene + geneByEnsemblId(ensemblId: String!): Gene + genesByPrefix(prefix: String!, taxonId: Int!, limit: Int = 30): [Gene!]! + genesByGenomicLocation(chr: String!, posStart: Int!, posStop: Int!, build: String = "hg19"): [Gene!]! } `; @@ -30,6 +40,13 @@ const rootValue = { clinvarByGeneEntrezId: clinvarResolvers.findByGeneEntrezId, clinvarByVariant: clinvarResolvers.findByVariant, clinvarCountsByEntrezId: clinvarResolvers.getCountsByEntrezId, + + geneBySymbol: geneResolvers.findByGeneSymbol, + geneByEntrezId: geneResolvers.findByEntrezId, + geneByHgncId: geneResolvers.findByHgncId, + geneByEnsemblId: geneResolvers.findByEnsemblId, + genesByPrefix: geneResolvers.findByPrefix, + genesByGenomicLocation: geneResolvers.findByGenomicLocation, }; // Create and export the GraphQL handler diff --git a/server/graphql/index.js b/server/graphql/index.js index 1f1259d..beb16da 100644 --- a/server/graphql/index.js +++ b/server/graphql/index.js @@ -10,6 +10,8 @@ * * Available queries: * - Clinvar: findByGeneSymbol, findByGeneEntrezId, findByVariant, getCountsByEntrezId + * - Gene: findByGeneSymbol, findByEntrezId, findByHgncId, findByEnsemblId, + * findByPrefix, findByGenomicLocation */ module.exports = { diff --git a/server/graphql/resolvers/gene.resolvers.js b/server/graphql/resolvers/gene.resolvers.js new file mode 100644 index 0000000..18379cd --- /dev/null +++ b/server/graphql/resolvers/gene.resolvers.js @@ -0,0 +1,118 @@ +const Genes = require('../../models/genes.model'); +const geneUtil = require('../../utils/gene'); + +/** + * Find gene by symbol and taxon ID + */ +const findByGeneSymbol = async ({ symbol, taxonId }) => { + try { + return await geneUtil.getBySymbol(taxonId, symbol); + } catch (error) { + console.error('Error fetching gene by symbol:', error); + throw new Error('Failed to fetch gene by symbol'); + } +}; + +/** + * Find gene by Entrez ID + */ +const findByEntrezId = async ({ entrezId }) => { + try { + return await geneUtil.getByEntrezId(entrezId); + } catch (error) { + console.error('Error fetching gene by Entrez ID:', error); + throw new Error('Failed to fetch gene by Entrez ID'); + } +}; + +/** + * Find gene by HGNC ID + */ +const findByHgncId = async ({ hgncId }) => { + try { + return await geneUtil.getByHgncId(hgncId); + } catch (error) { + console.error('Error fetching gene by HGNC ID:', error); + throw new Error('Failed to fetch gene by HGNC ID'); + } +}; + +/** + * Find gene by Ensembl ID + */ +const findByEnsemblId = async ({ ensemblId }) => { + try { + const gene = await Genes.findOne( + { 'xref.ensemblId': ensemblId }, + { _id: 0, clinVarIds: 0, dgvIds: 0, geno2mpIds: 0, gos: 0, phenotypes: 0 } + ).lean(); + + if (!gene) { + return null; + } + + // Convert alias to array if it's a string + if (gene.alias && typeof gene.alias === 'string') { + gene.alias = [gene.alias]; + } + + // Handle omimId format + if (gene.xref && gene.xref.omimId && gene.xref.omimId.length) { + gene.xref.omimId = gene.xref.omimId[0]; + } + + return gene; + } catch (error) { + console.error('Error fetching gene by Ensembl ID:', error); + throw new Error('Failed to fetch gene by Ensembl ID'); + } +}; + +/** + * Find genes by prefix search + */ +const findByPrefix = async ({ prefix, taxonId, limit = 30 }) => { + try { + const symbolRegex = new RegExp( + `^(${prefix.trim().split(/[^a-zA-Z0-9]+/g).join('|')})`, + taxonId === 7227 ? '' : 'i' + ); + + const genes = await Genes.find( + { taxonId, symbol: symbolRegex }, + { _id: 0, clinVarIds: 0, gos: 0, dgvIds: 0, geno2mpIds: 0, phenotypes: 0 }, + { limit, sort: { symbol: 1 } } + ).lean(); + + return genes.map(gene => { + if (gene.alias && typeof gene.alias === 'string') { + gene.alias = [gene.alias]; + } + return gene; + }); + } catch (error) { + console.error('Error fetching genes by prefix:', error); + throw new Error('Failed to fetch genes by prefix'); + } +}; + +/** + * Find genes by genomic location + */ +const findByGenomicLocation = async ({ chr, posStart, posStop, build = 'hg19' }) => { + try { + return await geneUtil.getByGenomicLocation(chr, posStart, posStop, build); + } catch (error) { + console.error('Error fetching genes by genomic location:', error); + throw new Error('Failed to fetch genes by genomic location'); + } +}; + +module.exports = { + findByGeneSymbol, + findByEntrezId, + findByHgncId, + findByEnsemblId, + findByPrefix, + findByGenomicLocation, +}; diff --git a/server/graphql/schemas/gene.schema.graphql b/server/graphql/schemas/gene.schema.graphql new file mode 100644 index 0000000..a872c31 --- /dev/null +++ b/server/graphql/schemas/gene.schema.graphql @@ -0,0 +1,31 @@ +""" +Cross-reference information for a gene +""" +type GeneXref { + ensemblId: String + omimId: String + mgiId: String + hgncId: String + pomBaseId: String +} + +""" +A gene entry +""" +type Gene { + taxonId: Int! + entrezId: Int! + symbol: String! + hgncId: Int + alias: [String] + name: String + locusType: String + status: String + xref: GeneXref + chr: String + hg19Start: Int + hg19Stop: Int + hg38Start: Int + hg38Stop: Int + uniprotKBId: String +} \ No newline at end of file From b4c810de4721b2108cdce5b64d91618744f6c5fe Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 10:33:40 -0600 Subject: [PATCH 04/10] feat(graphql): add DIOPT schema and resolvers * Added DIOPT-related GraphQL support: * DioptOrtholog * DioptDomain * DioptAlignment * Implemented resolvers to enable querying these DIOPT data structures. --- server/graphql/graphqlHandler.js | 14 ++- server/graphql/index.js | 2 + server/graphql/resolvers/diopt.resolvers.js | 111 ++++++++++++++++++++ server/graphql/schemas/diopt.schema.graphql | 51 +++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 server/graphql/resolvers/diopt.resolvers.js create mode 100644 server/graphql/schemas/diopt.schema.graphql diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index d8c8a6f..27709b1 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -6,28 +6,35 @@ const path = require('path'); // Import resolvers const clinvarResolvers = require('./resolvers/clinvar.resolvers'); const geneResolvers = require('./resolvers/gene.resolvers'); +const dioptResolvers = require('./resolvers/diopt.resolvers'); // Read schema files const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); const geneTypeDefs = readFileSync(path.join(__dirname, 'schemas/gene.schema.graphql'), 'utf8'); +const dioptTypeDefs = readFileSync(path.join(__dirname, 'schemas/diopt.schema.graphql'), 'utf8'); // Combine all schemas const typeDefs = ` ${clinvarTypeDefs} ${geneTypeDefs} + ${dioptTypeDefs} type Query { clinvarByGeneSymbol(symbol: String!): [Clinvar!]! clinvarByGeneEntrezId(entrezId: Int!): [Clinvar!]! clinvarByVariant(variant: String!, build: String = "hg19"): Clinvar clinvarCountsByEntrezId(entrezId: Int!): ClinvarCounts! - + geneBySymbol(symbol: String!, taxonId: Int!): Gene geneByEntrezId(entrezId: Int!): Gene geneByHgncId(hgncId: Int!): Gene geneByEnsemblId(ensemblId: String!): Gene genesByPrefix(prefix: String!, taxonId: Int!, limit: Int = 30): [Gene!]! genesByGenomicLocation(chr: String!, posStart: Int!, posStop: Int!, build: String = "hg19"): [Gene!]! + + dioptAlignmentByEntrezId(entrezId: Int!): DioptAlignment + dioptDomainsByEntrezId(entrezId: Int!): DioptDomainSet! + dioptOrthologsByEntrezId(entrezId: Int!): [DioptOrtholog!]! } `; @@ -47,6 +54,11 @@ const rootValue = { geneByEnsemblId: geneResolvers.findByEnsemblId, genesByPrefix: geneResolvers.findByPrefix, genesByGenomicLocation: geneResolvers.findByGenomicLocation, + + dioptAlignmentByEntrezId: dioptResolvers.findAlignmentsByEntrezId, + dioptDomainsByEntrezId: dioptResolvers.findDomainsByEntrezId, + dioptOrthologsByEntrezId: dioptResolvers.findOrthologsByEntrezId, + dioptOrthologsByTaxonId: dioptResolvers.findOrthologsByTaxonIds, }; // Create and export the GraphQL handler diff --git a/server/graphql/index.js b/server/graphql/index.js index beb16da..44ef9b0 100644 --- a/server/graphql/index.js +++ b/server/graphql/index.js @@ -12,6 +12,8 @@ * - Clinvar: findByGeneSymbol, findByGeneEntrezId, findByVariant, getCountsByEntrezId * - Gene: findByGeneSymbol, findByEntrezId, findByHgncId, findByEnsemblId, * findByPrefix, findByGenomicLocation + * - Diopt: findAlignmentsByEntrezId, findDomainsByEntrezId, findOrthologsByEntrezId, + * findOrthologsByTaxonIds */ module.exports = { diff --git a/server/graphql/resolvers/diopt.resolvers.js b/server/graphql/resolvers/diopt.resolvers.js new file mode 100644 index 0000000..a6b1b07 --- /dev/null +++ b/server/graphql/resolvers/diopt.resolvers.js @@ -0,0 +1,111 @@ +const DIOPTAlignments = require('../../models/diopt-alignments.model'); +const DIOPTOrtholog = require('../../models/diopt-ortholog.model'); +const Genes = require('../../models/genes.model'); + +/** + * Process alignment styles + */ +const processAlignmentStyles = (data) => { + for (let i = 0; i < data.length; ++i) { + const style = data[i].style; + if (!style) continue; + + for (let j = 0; j < style.length; ++j) { + let clsStr = ''; + if (style[j].indexOf('color: blue') !== -1) { + clsStr = 'mark-colon'; + } else if (style[j].indexOf('color: red') !== -1) { + clsStr = 'mark-asterisk'; + } else if (style[j].indexOf('color: purple') === -1) { + clsStr = 'mark-dot'; + } + style[j] = clsStr; + } + } +}; + +/** + * Find DIOPT alignments by Entrez ID + */ +const findAlignmentsByEntrezId = async ({ entrezId }) => { + try { + const doc = await DIOPTAlignments.findOne({ entrezId }, { _id: 0 }).lean(); + + if (doc && doc.data) { + processAlignmentStyles(doc.data); + } + + return doc; + } catch (error) { + console.error('Error fetching DIOPT alignments by Entrez ID:', error); + throw new Error('Failed to fetch DIOPT alignments by Entrez ID'); + } +}; + +/** + * Find DIOPT domains by Entrez ID + */ +const findDomainsByEntrezId = async ({ entrezId }) => { + try { + const gene = await Genes.findOne({ entrezId }, '-_id entrezId') + .populate('dioptDomains', '-_id') + .lean(); + + if (!gene) { + return { entrezId, domains: [] }; + } + + return { + entrezId, + domains: gene.dioptDomains || [] + }; + } catch (error) { + console.error('Error fetching DIOPT domains by Entrez ID:', error); + throw new Error('Failed to fetch DIOPT domains by Entrez ID'); + } +}; + +/** + * Find DIOPT orthologs by Entrez ID + */ +const findOrthologsByEntrezId = async ({ entrezId }) => { + try { + const docs = await DIOPTOrtholog.find({ entrezId1: entrezId }, { _id: 0 }) + .populate({ + path: 'gene2', + select: '-_id -location -lastModified -type -name -status -chr -alias -description -taxonId -clinVarIds -dgvIds -geno2mpIds -hg19Stop -hg19Start', + populate: [ + { + path: 'phenotypes.ontology', + select: 'name categories -_id' + }, + { + path: 'impcPhenotypes', + select: 'poId markerEntrezId markerMgiId poName alleleSymbol lifeStage sex pValue zygosity -_id', + populate: { + path: 'phenotype', + select: 'name categories -_id' + } + }, + { + path: 'gos.ontology', + select: 'name namespace agrSlimGoId -_id' + } + ], + }) + .populate({ path: 'gene1', select: 'symbol -_id' }) + .lean(); + + // Filter out entries where gene2 is null + return docs.filter(doc => doc.gene2 != null); + } catch (error) { + console.error('Error fetching DIOPT orthologs by Entrez ID:', error); + throw new Error('Failed to fetch DIOPT orthologs by Entrez ID'); + } +}; + +module.exports = { + findAlignmentsByEntrezId, + findDomainsByEntrezId, + findOrthologsByEntrezId +}; diff --git a/server/graphql/schemas/diopt.schema.graphql b/server/graphql/schemas/diopt.schema.graphql new file mode 100644 index 0000000..3efa5af --- /dev/null +++ b/server/graphql/schemas/diopt.schema.graphql @@ -0,0 +1,51 @@ +""" +DIOPT alignment data entry +""" +type DioptAlignment { + entrezId: Int! + data: [DioptAlignmentData] +} + +""" +DIOPT alignment data structure +""" +type DioptAlignmentData { + sequence: String + style: [String] + position: Int +} + +""" +DIOPT domain entry +""" +type DioptDomain { + name: String! + start: String! + end: String! + proteinId: String + externalId: String +} + +""" +DIOPT domains for a gene +""" +type DioptDomainSet { + entrezId: Int! + domains: [DioptDomain!]! +} + +""" +DIOPT ortholog relationship +""" +type DioptOrtholog { + taxonId1: Int! + entrezId1: Int! + taxonId2: Int! + entrezId2: Int! + score: Int! + bestScore: Boolean! + bestScoreRev: Boolean + confidence: String! + gene1: Gene + gene2: Gene +} \ No newline at end of file From 353a2603a53dff0c11f5a33fd23692324016f887 Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 10:34:14 -0600 Subject: [PATCH 05/10] docs(graphql): add GraphQL README --- server/graphql/README.md | 533 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 server/graphql/README.md diff --git a/server/graphql/README.md b/server/graphql/README.md new file mode 100644 index 0000000..bcf67dd --- /dev/null +++ b/server/graphql/README.md @@ -0,0 +1,533 @@ +# GraphQL API Documentation + +## Overview + +This GraphQL API provides access to genomic data from various databases including Clinvar, genes, and other genomic resources. + +## Endpoint + +- **GraphQL Endpoint**: `/graphql` +- **GraphiQL Interface**: `/graphiql` (development only) +- **Method**: POST +- **Content-Type**: `application/json` + +## Development + +In development mode, GraphiQL playground is available at `/graphiql` for interactive query testing. You can also access the GraphQL endpoint directly at `/graphql`. + +## Available Queries + +### Clinvar Queries + +#### 1. Get Clinvar variants by gene symbol + +```graphql +query GetClinvarByGeneSymbol($symbol: String!) { + clinvarByGeneSymbol(symbol: $symbol) { + chr + start + stop + ref + alt + uid + condition + title + significance { + description + lastEvaluated + reviewStatus + } + band + } +} +``` + +**Variables:** +```json +{ + "symbol": "BRCA1" +} +``` + +#### 2. Get Clinvar variants by gene Entrez ID + +```graphql +query GetClinvarByGeneEntrezId($entrezId: Int!) { + clinvarByGeneEntrezId(entrezId: $entrezId) { + chr + start + stop + condition + title + significance { + description + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +#### 3. Get Clinvar variant by specific variant + +```graphql +query GetClinvarByVariant($variant: String!, $build: String) { + clinvarByVariant(variant: $variant, build: $build) { + chr + start + stop + ref + alt + condition + title + significance { + description + reviewStatus + } + } +} +``` + +**Variables:** +```json +{ + "variant": "17-43094454-A-T", + "build": "hg19" +} +``` + +#### 4. Get Clinvar variant counts by gene Entrez ID + +```graphql +query GetClinvarCounts($entrezId: Int!) { + clinvarCountsByEntrezId(entrezId: $entrezId) { + pathogenic + likelyPathogenic + likelyBenign + benign + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +### Gene Queries + +#### 1. Get gene by symbol and taxon ID + +```graphql +query GetGeneBySymbol($symbol: String!, $taxonId: Int!) { + geneBySymbol(symbol: $symbol, taxonId: $taxonId) { + entrezId + symbol + name + alias + taxonId + locusType + status + chr + hg19Start + hg19Stop + xref { + ensemblId + omimId + hgncId + } + } +} +``` + +**Variables:** +```json +{ + "symbol": "BRCA1", + "taxonId": 9606 +} +``` + +#### 2. Get gene by Entrez ID + +```graphql +query GetGeneByEntrezId($entrezId: Int!) { + geneByEntrezId(entrezId: $entrezId) { + entrezId + symbol + name + alias + taxonId + xref { + ensemblId + omimId + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +#### 3. Get gene by HGNC ID + +```graphql +query GetGeneByHgncId($hgncId: Int!) { + geneByHgncId(hgncId: $hgncId) { + entrezId + symbol + name + hgncId + } +} +``` + +**Variables:** +```json +{ + "hgncId": 1100 +} +``` + +#### 4. Get gene by Ensembl ID + +```graphql +query GetGeneByEnsemblId($ensemblId: String!) { + geneByEnsemblId(ensemblId: $ensemblId) { + entrezId + symbol + name + xref { + ensemblId + omimId + } + } +} +``` + +**Variables:** +```json +{ + "ensemblId": "ENSG00000012048" +} +``` + +#### 5. Search genes by prefix + +```graphql +query GetGenesByPrefix($prefix: String!, $taxonId: Int!, $limit: Int) { + genesByPrefix(prefix: $prefix, taxonId: $taxonId, limit: $limit) { + entrezId + symbol + name + alias + } +} +``` + +**Variables:** +```json +{ + "prefix": "BRCA", + "taxonId": 9606, + "limit": 10 +} +``` + +#### 6. Get genes by genomic location + +```graphql +query GetGenesByGenomicLocation($chr: String!, $posStart: Int!, $posStop: Int!, $build: String) { + genesByGenomicLocation(chr: $chr, posStart: $posStart, posStop: $posStop, build: $build) { + entrezId + symbol + name + chr + hg19Start + hg19Stop + } +} +``` + +**Variables:** +```json +{ + "chr": "17", + "posStart": 43000000, + "posStop": 44000000, + "build": "hg19" +} +``` + +### DIOPT Queries + +#### 1. Get DIOPT alignments by Entrez ID + +```graphql +query GetDioptAlignmentByEntrezId($entrezId: Int!) { + dioptAlignmentByEntrezId(entrezId: $entrezId) { + entrezId + data { + sequence + style + position + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +#### 2. Get DIOPT domains by Entrez ID + +```graphql +query GetDioptDomainsByEntrezId($entrezId: Int!) { + dioptDomainsByEntrezId(entrezId: $entrezId) { + entrezId + domains { + name + start + end + proteinId + externalId + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +#### 3. Get DIOPT orthologs by Entrez ID + +```graphql +query GetDioptOrthologsByEntrezId($entrezId: Int!) { + dioptOrthologsByEntrezId(entrezId: $entrezId) { + taxonId1 + entrezId1 + taxonId2 + entrezId2 + score + bestScore + confidence + gene1 { + symbol + } + gene2 { + symbol + entrezId + name + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 672 +} +``` + +#### 4. Get DIOPT orthologs by taxon IDs + +```graphql +query GetDioptOrthologsByTaxonId($taxonId1: Int!, $taxonId2: Int!, $limit: Int) { + dioptOrthologsByTaxonId(taxonId1: $taxonId1, taxonId2: $taxonId2, limit: $limit) { + entrezId1 + entrezId2 + score + confidence + gene1 { + symbol + entrezId + } + gene2 { + symbol + entrezId + } + } +} +``` + +**Variables:** +```json +{ + "taxonId1": 9606, + "taxonId2": 7227, + "limit": 50 +} +``` + +## Example Usage with curl + +```bash +curl -X POST \ + http://localhost:3000/graphql \ + -H 'Content-Type: application/json' \ + -d '{ + "query": "query GetClinvarByGeneSymbol($symbol: String!) { clinvarByGeneSymbol(symbol: $symbol) { chr start stop condition title significance { description } } }", + "variables": { + "symbol": "BRCA1" + } + }' +``` + +## Interactive Development + +For easier development and testing, you can use the GraphiQL interface: + +1. **Start the server** in development mode +2. **Open your browser** to `http://localhost:3000/graphiql` +3. **Use the interactive interface** to: + - Browse the schema documentation + - Write and test queries with autocomplete + - View query results in real-time + - Explore available types and fields + +### Example GraphiQL Query + +You can copy and paste this into the GraphiQL interface: + +```graphql +{ + geneBySymbol(symbol: "BRCA1", taxonId: 9606) { + entrezId + symbol + name + chr + hg19Start + hg19Stop + } + + clinvarByGeneSymbol(symbol: "BRCA1") { + chr + start + condition + significance { + description + } + } + + dioptOrthologsByEntrezId(entrezId: 672) { + taxonId2 + entrezId2 + score + confidence + gene2 { + symbol + name + } + } +} +``` + +## Error Handling + +All GraphQL queries return structured error messages in case of failures. Common error scenarios: + +- Invalid variant format +- Gene symbol not found +- Database connection issues +- Invalid parameters + +## Schema Types + +### Clinvar +- `chr`: String! - Chromosome +- `start`: Int! - Start position +- `stop`: Int! - Stop position +- `ref`: String - Reference allele +- `alt`: String - Alternative allele +- `uid`: Int - Clinvar UID +- `condition`: String - Associated condition +- `title`: String - Variant title +- `significance`: ClinvarSignificance - Clinical significance +- `band`: String - Chromosomal band + +### ClinvarSignificance +- `description`: String - Significance description +- `lastEvaluated`: String - Last evaluation date +- `reviewStatus`: String - Review status + +### ClinvarCounts +- `pathogenic`: Int! - Number of pathogenic variants +- `likelyPathogenic`: Int! - Number of likely pathogenic variants +- `likelyBenign`: Int! - Number of likely benign variants +- `benign`: Int! - Number of benign variants + +### Gene +- `taxonId`: Int! - NCBI Taxonomy ID +- `entrezId`: Int! - Entrez Gene ID +- `symbol`: String! - Gene symbol +- `hgncId`: Int - HGNC ID +- `alias`: [String] - Gene aliases +- `name`: String - Gene name +- `locusType`: String - Locus type +- `status`: String - Gene status +- `xref`: GeneXref - Cross-references +- `chr`: String - Chromosome +- `hg19Start`: Int - Start position (hg19) +- `hg19Stop`: Int - Stop position (hg19) +- `hg38Start`: Int - Start position (hg38) +- `hg38Stop`: Int - Stop position (hg38) +- `uniprotKBId`: String - UniProt KB ID + +### GeneXref +- `ensemblId`: String - Ensembl ID +- `omimId`: String - OMIM ID +- `mgiId`: String - MGI ID +- `hgncId`: String - HGNC ID +- `pomBaseId`: String - PomBase ID + +### DioptAlignment +- `entrezId`: Int! - Entrez Gene ID +- `data`: [DioptAlignmentData] - Alignment data entries + +### DioptAlignmentData +- `sequence`: String - Aligned sequence +- `style`: [String] - Styling information for alignment visualization +- `position`: Int - Position in alignment + +### DioptDomain +- `name`: String! - Domain name +- `start`: String! - Domain start position +- `end`: String! - Domain end position +- `proteinId`: String - Protein identifier +- `externalId`: String - External database identifier + +### DioptDomainSet +- `entrezId`: Int! - Entrez Gene ID +- `domains`: [DioptDomain!]! - List of domains + +### DioptOrtholog +- `taxonId1`: Int! - Source organism taxonomy ID +- `entrezId1`: Int! - Source gene Entrez ID +- `taxonId2`: Int! - Target organism taxonomy ID +- `entrezId2`: Int! - Target gene Entrez ID +- `score`: Int! - Orthology confidence score +- `bestScore`: Boolean! - Whether this is the best score +- `bestScoreRev`: Boolean - Whether this is the best reverse score +- `confidence`: String! - Confidence level (High/Moderate/Low) +- `gene1`: Gene - Source gene information +- `gene2`: Gene - Target gene information \ No newline at end of file From 537422fed270bb0243b4ed1ed4fd03bb7777f84b Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 11:02:37 -0600 Subject: [PATCH 06/10] feat(graphql): add PO schema and resolver --- server/graphql/README.md | 182 +++++++++++++++++- server/graphql/graphqlHandler.js | 16 ++ server/graphql/index.js | 4 +- server/graphql/resolvers/diopt.resolvers.js | 14 +- .../resolvers/phenotype-ontology.resolvers.js | 86 +++++++++ .../schemas/phenotype-ontology.schema.graphql | 20 ++ 6 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 server/graphql/resolvers/phenotype-ontology.resolvers.js create mode 100644 server/graphql/schemas/phenotype-ontology.schema.graphql diff --git a/server/graphql/README.md b/server/graphql/README.md index bcf67dd..8954b2e 100644 --- a/server/graphql/README.md +++ b/server/graphql/README.md @@ -2,7 +2,7 @@ ## Overview -This GraphQL API provides access to genomic data from various databases including Clinvar, genes, and other genomic resources. +This GraphQL API provides access to genomic data from various databases including Clinvar, genes, DIOPT, phenotype ontology, and other genomic resources. ## Endpoint @@ -379,6 +379,162 @@ query GetDioptOrthologsByTaxonId($taxonId1: Int!, $taxonId2: Int!, $limit: Int) } ``` +### PhenotypeOntology Queries + +#### 1. Get phenotype ontology term by PO ID + +```graphql +query GetPhenotypeOntologyByPoId($poId: String!) { + phenotypeOntologyByPoId(poId: $poId) { + id + name + def + namespace + taxonId + is_a + categories { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "poId": "PO:0000001" +} +``` + +#### 2. Search phenotype ontology terms by name + +```graphql +query GetPhenotypeOntologyByName($name: String!, $limit: Int, $start: Int) { + phenotypeOntologyByName(name: $name, limit: $limit, start: $start) { + id + name + def + namespace + categories { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "name": "leaf", + "limit": 10, + "start": 0 +} +``` + +#### 3. Get phenotype ontology terms by taxon ID + +```graphql +query GetPhenotypeOntologyByTaxonId($taxonId: Int!, $limit: Int, $start: Int) { + phenotypeOntologyByTaxonId(taxonId: $taxonId, limit: $limit, start: $start) { + id + name + namespace + categories { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "taxonId": 3702, + "limit": 20, + "start": 0 +} +``` + +#### 4. Get phenotype ontology terms by namespace + +```graphql +query GetPhenotypeOntologyByNamespace($namespace: String!, $limit: Int, $start: Int) { + phenotypeOntologyByNamespace(namespace: $namespace, limit: $limit, start: $start) { + id + name + def + taxonId + } +} +``` + +**Variables:** +```json +{ + "namespace": "plant_anatomy", + "limit": 30, + "start": 0 +} +``` + +#### 5. Get phenotype ontology terms by category + +```graphql +query GetPhenotypeOntologyByCategory($categoryId: Int!, $limit: Int, $start: Int) { + phenotypeOntologyByCategory(categoryId: $categoryId, limit: $limit, start: $start) { + id + name + def + categories { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "categoryId": 1, + "limit": 25, + "start": 0 +} +``` + +### Pagination Example + +For queries that support pagination, you can use the `start` and `limit` parameters to retrieve data in chunks: + +```graphql +# Get first 20 results +query GetFirstPage { + phenotypeOntologyByName(name: "leaf", limit: 20, start: 0) { + id + name + } +} + +# Get next 20 results (page 2) +query GetSecondPage { + phenotypeOntologyByName(name: "leaf", limit: 20, start: 20) { + id + name + } +} + +# Get third page +query GetThirdPage { + phenotypeOntologyByName(name: "leaf", limit: 20, start: 40) { + id + name + } +} +``` + ## Example Usage with curl ```bash @@ -439,6 +595,15 @@ You can copy and paste this into the GraphiQL interface: name } } + + phenotypeOntologyByName(name: "leaf", limit: 5) { + id + name + namespace + categories { + name + } + } } ``` @@ -530,4 +695,17 @@ All GraphQL queries return structured error messages in case of failures. Common - `bestScoreRev`: Boolean - Whether this is the best reverse score - `confidence`: String! - Confidence level (High/Moderate/Low) - `gene1`: Gene - Source gene information -- `gene2`: Gene - Target gene information \ No newline at end of file +- `gene2`: Gene - Target gene information + +### PhenotypeOntology +- `id`: String! - Phenotype ontology ID (PO ID) +- `name`: String - Term name +- `def`: String - Term definition +- `namespace`: String - Ontology namespace +- `taxonId`: Int - Organism taxonomy ID +- `is_a`: [String] - Parent term IDs +- `categories`: [PhenotypeOntologyCategory] - Associated categories + +### PhenotypeOntologyCategory +- `id`: Int - Category identifier +- `name`: String - Category name \ No newline at end of file diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index 27709b1..b60d650 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -7,17 +7,20 @@ const path = require('path'); const clinvarResolvers = require('./resolvers/clinvar.resolvers'); const geneResolvers = require('./resolvers/gene.resolvers'); const dioptResolvers = require('./resolvers/diopt.resolvers'); +const phenotypeOntologyResolvers = require('./resolvers/phenotype-ontology.resolvers'); // Read schema files const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); const geneTypeDefs = readFileSync(path.join(__dirname, 'schemas/gene.schema.graphql'), 'utf8'); const dioptTypeDefs = readFileSync(path.join(__dirname, 'schemas/diopt.schema.graphql'), 'utf8'); +const phenotypeOntologyTypeDefs = readFileSync(path.join(__dirname, 'schemas/phenotype-ontology.schema.graphql'), 'utf8'); // Combine all schemas const typeDefs = ` ${clinvarTypeDefs} ${geneTypeDefs} ${dioptTypeDefs} + ${phenotypeOntologyTypeDefs} type Query { clinvarByGeneSymbol(symbol: String!): [Clinvar!]! @@ -35,6 +38,13 @@ const typeDefs = ` dioptAlignmentByEntrezId(entrezId: Int!): DioptAlignment dioptDomainsByEntrezId(entrezId: Int!): DioptDomainSet! dioptOrthologsByEntrezId(entrezId: Int!): [DioptOrtholog!]! + dioptOrthologsByTaxonId(taxonId1: Int!, taxonId2: Int!, limit: Int = 100): [DioptOrtholog!]! + + phenotypeOntologyByPoId(poId: String!): PhenotypeOntology + phenotypeOntologyByName(name: String!, limit: Int = 50, start: Int = 0): [PhenotypeOntology!]! + phenotypeOntologyByTaxonId(taxonId: Int!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! + phenotypeOntologyByNamespace(namespace: String!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! + phenotypeOntologyByCategory(categoryId: Int!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! } `; @@ -59,6 +69,12 @@ const rootValue = { dioptDomainsByEntrezId: dioptResolvers.findDomainsByEntrezId, dioptOrthologsByEntrezId: dioptResolvers.findOrthologsByEntrezId, dioptOrthologsByTaxonId: dioptResolvers.findOrthologsByTaxonIds, + + phenotypeOntologyByPoId: phenotypeOntologyResolvers.findByPoId, + phenotypeOntologyByName: phenotypeOntologyResolvers.findByName, + phenotypeOntologyByTaxonId: phenotypeOntologyResolvers.findByTaxonId, + phenotypeOntologyByNamespace: phenotypeOntologyResolvers.findByNamespace, + phenotypeOntologyByCategory: phenotypeOntologyResolvers.findByCategory, }; // Create and export the GraphQL handler diff --git a/server/graphql/index.js b/server/graphql/index.js index 44ef9b0..85ee6e9 100644 --- a/server/graphql/index.js +++ b/server/graphql/index.js @@ -2,7 +2,7 @@ * GraphQL API module for MARRVEL * * This module provides GraphQL API endpoints for various data sources - * including Clinvar, genes, DIOPT, and other genomic databases. + * including Clinvar, genes, DIOPT, phenotype ontology, and other genomic databases. * * Usage: * - Access GraphQL playground at /graphql (in development) @@ -14,6 +14,8 @@ * findByPrefix, findByGenomicLocation * - Diopt: findAlignmentsByEntrezId, findDomainsByEntrezId, findOrthologsByEntrezId, * findOrthologsByTaxonIds + * - PhenotypeOntology: findByPoId, findByName, findByTaxonId, findByNamespace, + * findByCategory */ module.exports = { diff --git a/server/graphql/resolvers/diopt.resolvers.js b/server/graphql/resolvers/diopt.resolvers.js index a6b1b07..75b4f38 100644 --- a/server/graphql/resolvers/diopt.resolvers.js +++ b/server/graphql/resolvers/diopt.resolvers.js @@ -47,7 +47,7 @@ const findAlignmentsByEntrezId = async ({ entrezId }) => { */ const findDomainsByEntrezId = async ({ entrezId }) => { try { - const gene = await Genes.findOne({ entrezId }, '-_id entrezId') + const gene = await Genes.findOne({ entrezId }, '-_id -clinVarIds -dgvIds -geno2mpIds') .populate('dioptDomains', '-_id') .lean(); @@ -73,27 +73,27 @@ const findOrthologsByEntrezId = async ({ entrezId }) => { const docs = await DIOPTOrtholog.find({ entrezId1: entrezId }, { _id: 0 }) .populate({ path: 'gene2', - select: '-_id -location -lastModified -type -name -status -chr -alias -description -taxonId -clinVarIds -dgvIds -geno2mpIds -hg19Stop -hg19Start', + select: '-_id -clinVarIds -dgvIds -geno2mpIds', populate: [ { path: 'phenotypes.ontology', - select: 'name categories -_id' + select: '-_id' }, { path: 'impcPhenotypes', - select: 'poId markerEntrezId markerMgiId poName alleleSymbol lifeStage sex pValue zygosity -_id', + select: '-_id', populate: { path: 'phenotype', - select: 'name categories -_id' + select: '-_id' } }, { path: 'gos.ontology', - select: 'name namespace agrSlimGoId -_id' + select: '-_id' } ], }) - .populate({ path: 'gene1', select: 'symbol -_id' }) + .populate({ path: 'gene1', select: '-_id -clinVarIds -dgvIds -geno2mpIds' }) .lean(); // Filter out entries where gene2 is null diff --git a/server/graphql/resolvers/phenotype-ontology.resolvers.js b/server/graphql/resolvers/phenotype-ontology.resolvers.js new file mode 100644 index 0000000..11d991c --- /dev/null +++ b/server/graphql/resolvers/phenotype-ontology.resolvers.js @@ -0,0 +1,86 @@ +const PhenotypeOntologyTerms = require('../../models/phenotype-ontology-terms.model'); + +/** + * Find phenotype ontology term by PO ID + */ +const findByPoId = async ({ poId }) => { + try { + return await PhenotypeOntologyTerms.findOne({ id: poId }, { _id: 0 }).lean(); + } catch (error) { + console.error('Error fetching phenotype ontology by PO ID:', error); + throw new Error('Failed to fetch phenotype ontology by PO ID'); + } +}; + +/** + * Find phenotype ontology terms by name search + */ +const findByName = async ({ name, limit = 50, start = 0 }) => { + try { + const nameRegex = new RegExp(name, 'i'); + return await PhenotypeOntologyTerms.find( + { name: nameRegex }, + { _id: 0 }, + { limit, skip: start, sort: { id: 1 } } + ).lean(); + } catch (error) { + console.error('Error fetching phenotype ontology by name:', error); + throw new Error('Failed to fetch phenotype ontology by name'); + } +}; + +/** + * Find phenotype ontology terms by taxon ID + */ +const findByTaxonId = async ({ taxonId, limit = 100, start = 0 }) => { + try { + return await PhenotypeOntologyTerms.find( + { taxonId }, + { _id: 0 }, + { limit, skip: start, sort: { id: 1 } } + ).lean(); + } catch (error) { + console.error('Error fetching phenotype ontology by taxon ID:', error); + throw new Error('Failed to fetch phenotype ontology by taxon ID'); + } +}; + +/** + * Find phenotype ontology terms by namespace + */ +const findByNamespace = async ({ namespace, limit = 100, start = 0 }) => { + try { + return await PhenotypeOntologyTerms.find( + { namespace }, + { _id: 0 }, + { limit, skip: start, sort: { id: 1 } } + ).lean(); + } catch (error) { + console.error('Error fetching phenotype ontology by namespace:', error); + throw new Error('Failed to fetch phenotype ontology by namespace'); + } +}; + +/** + * Find phenotype ontology terms by category + */ +const findByCategory = async ({ categoryId, limit = 100, start = 0 }) => { + try { + return await PhenotypeOntologyTerms.find( + { 'categories.id': categoryId }, + { _id: 0 }, + { limit, skip: start, sort: { id: 1 } } + ).lean(); + } catch (error) { + console.error('Error fetching phenotype ontology by category:', error); + throw new Error('Failed to fetch phenotype ontology by category'); + } +}; + +module.exports = { + findByPoId, + findByName, + findByTaxonId, + findByNamespace, + findByCategory, +}; diff --git a/server/graphql/schemas/phenotype-ontology.schema.graphql b/server/graphql/schemas/phenotype-ontology.schema.graphql new file mode 100644 index 0000000..65d3e24 --- /dev/null +++ b/server/graphql/schemas/phenotype-ontology.schema.graphql @@ -0,0 +1,20 @@ +""" +A category within a phenotype ontology +""" +type PhenotypeOntologyCategory { + id: Int + name: String +} + +""" +A phenotype ontology term +""" +type PhenotypeOntology { + id: String! + name: String + def: String + namespace: String + taxonId: Int + is_a: [String] + categories: [PhenotypeOntologyCategory] +} \ No newline at end of file From a3826918568d5f0eda894260146f52b3d9bcb4e5 Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 11:35:58 -0600 Subject: [PATCH 07/10] feat(graphql): add Pharos schema Add comprehensive Pharos target, drug, and ligand GraphQL schemas and resolver --- server/graphql/README.md | 187 ++++++++++++++++++- server/graphql/graphqlHandler.js | 14 ++ server/graphql/resolvers/pharos.resolver.js | 104 +++++++++++ server/graphql/schemas/pharos.schema.graphql | 45 +++++ 4 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 server/graphql/resolvers/pharos.resolver.js create mode 100644 server/graphql/schemas/pharos.schema.graphql diff --git a/server/graphql/README.md b/server/graphql/README.md index 8954b2e..670990c 100644 --- a/server/graphql/README.md +++ b/server/graphql/README.md @@ -708,4 +708,189 @@ All GraphQL queries return structured error messages in case of failures. Common ### PhenotypeOntologyCategory - `id`: Int - Category identifier -- `name`: String - Category name \ No newline at end of file +- `name`: String - Category name + +### PharosTarget +- `id`: Int! - Pharos target identifier +- `name`: String! - Target name +- `gene`: String - Associated gene symbol +- `accession`: String - Protein accession identifier +- `structureRefId`: String! - Structure reference ID +- `description`: String - Target description +- `idgFamily`: String - IDG family classification +- `idgTDL`: String - IDG target development level +- `idgDevLevel`: String - IDG development level +- `self`: String - Self reference URL +- `drugIds`: [Int!]! - Array of associated drug IDs +- `ligandIds`: [Int!]! - Array of associated ligand IDs +- `drugs`: [PharosDrug] - Associated drugs (populated from drugIds) +- `ligands`: [PharosLigand] - Associated ligands (populated from ligandIds) + +### PharosDrug +- `id`: Int! - Drug identifier +- `name`: String! - Drug name +- `description`: String - Drug description +- `self`: String - Self reference URL +- `namespace`: String - Drug namespace +- `structureRefId`: String - Structure reference ID +- `idgDevLevel`: String - IDG development level + +### PharosLigand +- `id`: Int! - Ligand identifier +- `name`: String! - Ligand name +- `description`: String - Ligand description +- `self`: String - Self reference URL +- `namespace`: String - Ligand namespace +- `structureRefId`: String - Structure reference ID +- `idgDevLevel`: String - IDG development level + +## Pharos Queries + +### Find Target by ID + +Find a specific Pharos target by its ID. + +**Query:** +```graphql +query($id: Int!) { + pharosTargetById(id: $id) { + id + name + gene + accession + description + idgFamily + idgTDL + drugs { + id + name + description + } + ligands { + id + name + description + } + } +} +``` + +**Variables:** +```json +{ + "id": 1234 +} +``` + +### Find Targets by IDs + +Find multiple Pharos targets by their IDs with pagination support. + +**Query:** +```graphql +query($ids: [Int!]!, $limit: Int, $start: Int) { + pharosTargetsByIds(ids: $ids, limit: $limit, start: $start) { + id + name + gene + accession + description + idgTDL + drugs { + id + name + } + ligands { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "ids": [1234, 5678, 9012], + "limit": 50, + "start": 0 +} +``` + +### Find Targets by Gene Entrez ID + +Find Pharos targets associated with a specific gene via Entrez ID. + +**Query:** +```graphql +query($entrezId: Int!, $limit: Int, $start: Int) { + pharosTargetsByGeneEntrezId(entrezId: $entrezId, limit: $limit, start: $start) { + id + name + gene + accession + description + idgFamily + idgTDL + drugs { + id + name + description + } + ligands { + id + name + description + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 7157, + "limit": 100, + "start": 0 +} +``` + +### Combining Gene and Pharos Data + +To get both gene information and associated Pharos targets, use separate queries in a single GraphQL request: + +**Query:** +```graphql +query($entrezId: Int!) { + gene: geneByEntrezId(entrezId: $entrezId) { + entrezId + symbol + name + description + } + + pharosTargets: pharosTargetsByGeneEntrezId(entrezId: $entrezId) { + id + name + accession + description + idgFamily + idgTDL + drugs { + id + name + } + ligands { + id + name + } + } +} +``` + +**Variables:** +```json +{ + "entrezId": 7157 +} +``` \ No newline at end of file diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index b60d650..8aa7ed6 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -8,12 +8,14 @@ const clinvarResolvers = require('./resolvers/clinvar.resolvers'); const geneResolvers = require('./resolvers/gene.resolvers'); const dioptResolvers = require('./resolvers/diopt.resolvers'); const phenotypeOntologyResolvers = require('./resolvers/phenotype-ontology.resolvers'); +const pharosResolvers = require('./resolvers/pharos.resolver'); // Read schema files const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); const geneTypeDefs = readFileSync(path.join(__dirname, 'schemas/gene.schema.graphql'), 'utf8'); const dioptTypeDefs = readFileSync(path.join(__dirname, 'schemas/diopt.schema.graphql'), 'utf8'); const phenotypeOntologyTypeDefs = readFileSync(path.join(__dirname, 'schemas/phenotype-ontology.schema.graphql'), 'utf8'); +const pharosTypeDefs = readFileSync(path.join(__dirname, 'schemas/pharos.schema.graphql'), 'utf8'); // Combine all schemas const typeDefs = ` @@ -21,6 +23,7 @@ const typeDefs = ` ${geneTypeDefs} ${dioptTypeDefs} ${phenotypeOntologyTypeDefs} + ${pharosTypeDefs} type Query { clinvarByGeneSymbol(symbol: String!): [Clinvar!]! @@ -45,6 +48,10 @@ const typeDefs = ` phenotypeOntologyByTaxonId(taxonId: Int!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! phenotypeOntologyByNamespace(namespace: String!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! phenotypeOntologyByCategory(categoryId: Int!, limit: Int = 100, start: Int = 0): [PhenotypeOntology!]! + + pharosTargetById(id: Int!): PharosTarget + pharosTargetsByIds(ids: [Int!]!, limit: Int = 100, start: Int = 0): [PharosTarget!]! + pharosTargetsByGeneEntrezId(entrezId: Int!, limit: Int = 100, start: Int = 0): [PharosTarget!]! } `; @@ -75,6 +82,13 @@ const rootValue = { phenotypeOntologyByTaxonId: phenotypeOntologyResolvers.findByTaxonId, phenotypeOntologyByNamespace: phenotypeOntologyResolvers.findByNamespace, phenotypeOntologyByCategory: phenotypeOntologyResolvers.findByCategory, + + pharosTargetById: pharosResolvers.pharosTargetById, + pharosTargetsByIds: pharosResolvers.pharosTargetsByIds, + pharosTargetsByGeneEntrezId: pharosResolvers.pharosTargetsByGeneEntrezId, + + // Type resolvers + PharosTarget: pharosResolvers.PharosTarget, }; // Create and export the GraphQL handler diff --git a/server/graphql/resolvers/pharos.resolver.js b/server/graphql/resolvers/pharos.resolver.js new file mode 100644 index 0000000..3fa46d6 --- /dev/null +++ b/server/graphql/resolvers/pharos.resolver.js @@ -0,0 +1,104 @@ +const Genes = require('../../models/genes.model'); +const PharosTargets = require('../../models/pharos-targets.model'); +const PharosDrugs = require('../../models/pharos-drugs.model'); +const PharosLigands = require('../../models/pharos-ligands.model'); + +/** + * Pharos GraphQL resolvers + */ +const pharosResolvers = { + // Root query resolvers + pharosTargetById: async (args) => { + try { + const target = await PharosTargets.findOne({ id: args.id }); + if (!target) { + throw new Error(`PharosTarget with id ${args.id} not found`); + } + return target; + } catch (error) { + console.error('Error in pharosTargetById resolver:', error); + throw error; + } + }, + + pharosTargetsByIds: async (args) => { + try { + const { ids, limit = 100, start = 0 } = args; + + if (ids.length === 0) { + return []; + } + + const targets = await PharosTargets + .find({ id: { $in: ids } }) + .skip(start) + .limit(limit); + + return targets; + } catch (error) { + console.error('Error in pharosTargetsByIds resolver:', error); + throw error; + } + }, + + pharosTargetsByGeneEntrezId: async (args) => { + try { + const { entrezId, limit = 100, start = 0 } = args; + + // Use the existing controller logic to get targets by gene + const gene = await Genes.findOne({ entrezId }); + if (!gene || !gene.pharosTargetIds || gene.pharosTargetIds.length === 0) { + return []; + } + + const targets = await PharosTargets + .find({ id: { $in: gene.pharosTargetIds } }) + .skip(start) + .limit(limit); + + return targets; + } catch (error) { + console.error('Error in pharosTargetsByGeneEntrezId resolver:', error); + throw error; + } + }, + + // Type field resolvers + PharosTarget: { + drugs: async (parent) => { + try { + if (!parent.drugIds || parent.drugIds.length === 0) { + return []; + } + + const drugs = await PharosDrugs.find({ + id: { $in: parent.drugIds } + }); + + return drugs; + } catch (error) { + console.error('Error resolving PharosTarget.drugs:', error); + return []; + } + }, + + ligands: async (parent) => { + try { + if (!parent.ligandIds || parent.ligandIds.length === 0) { + return []; + } + + const ligands = await PharosLigands.find({ + id: { $in: parent.ligandIds } + }); + + return ligands; + } catch (error) { + console.error('Error resolving PharosTarget.ligands:', error); + return []; + } + } + } +}; + +module.exports = pharosResolvers; diff --git a/server/graphql/schemas/pharos.schema.graphql b/server/graphql/schemas/pharos.schema.graphql new file mode 100644 index 0000000..d312671 --- /dev/null +++ b/server/graphql/schemas/pharos.schema.graphql @@ -0,0 +1,45 @@ +""" +A Pharos drug +""" +type PharosDrug { + id: Int! + name: String! + description: String + self: String + namespace: String + structureRefId: String + idgDevLevel: String +} + +""" +A Pharos ligand +""" +type PharosLigand { + id: Int! + name: String! + description: String + self: String + namespace: String + structureRefId: String + idgDevLevel: String +} + +""" +A Pharos target +""" +type PharosTarget { + id: Int! + name: String! + gene: String + accession: String + structureRefId: String! + description: String + idgFamily: String + idgTDL: String + idgDevLevel: String + self: String + drugIds: [Int!]! + ligandIds: [Int!]! + drugs: [PharosDrug] + ligands: [PharosLigand] +} \ No newline at end of file From 0d7607c5beb0140b90a8c8bcb432608b0d917e53 Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 13:40:49 -0600 Subject: [PATCH 08/10] feat(graphql): add String --- server/graphql/README.md | 60 ++++++++++++++++++++ server/graphql/graphqlHandler.js | 15 ++++- server/graphql/resolvers/string.resolvers.js | 40 +++++++++++++ server/graphql/schemas/string.schema.graphql | 10 ++++ server/models/string-interactions.model.js | 26 +++++++++ 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 server/graphql/resolvers/string.resolvers.js create mode 100644 server/graphql/schemas/string.schema.graphql create mode 100644 server/models/string-interactions.model.js diff --git a/server/graphql/README.md b/server/graphql/README.md index 670990c..fc4bb59 100644 --- a/server/graphql/README.md +++ b/server/graphql/README.md @@ -893,4 +893,64 @@ query($entrezId: Int!) { { "entrezId": 7157 } +``` + +### StringInteraction +- `ensemblId1`: String! - First protein Ensembl ID (not gene ID) +- `ensemblId2`: String! - Second protein Ensembl ID (not gene ID) +- `experiments`: Int! - Experimental evidence score +- `database`: Int! - Database evidence score +- `combExpDb`: Int! - Combined experimental and database score + +## String Queries + +### Find Interactions by Ensembl Protein ID + +Find STRING protein-protein interactions for a protein by its Ensembl protein ID. + +**Query:** +```graphql +query($ensemblId: String!, $limit: Int, $start: Int) { + stringInteractionsByEnsemblId(ensemblId: $ensemblId, limit: $limit, start: $start) { + ensemblId1 + ensemblId2 + experiments + database + combExpDb + } +} +``` + +**Variables:** +```json +{ + "ensemblId": "ENSP00000000233", + "limit": 100, + "start": 0 +} +``` + +### Find Interaction Between Two Proteins + +Find the specific STRING interaction between two proteins using their Ensembl protein IDs. This query searches bidirectionally. + +**Query:** +```graphql +query($ensemblId1: String!, $ensemblId2: String!) { + stringInteractionBetweenProteins(ensemblId1: $ensemblId1, ensemblId2: $ensemblId2) { + ensemblId1 + ensemblId2 + experiments + database + combExpDb + } +} +``` + +**Variables:** +```json +{ + "ensemblId1": "ENSP00000000233", + "ensemblId2": "ENSP00000000412" +} ``` \ No newline at end of file diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index 8aa7ed6..9d861cb 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -9,6 +9,7 @@ const geneResolvers = require('./resolvers/gene.resolvers'); const dioptResolvers = require('./resolvers/diopt.resolvers'); const phenotypeOntologyResolvers = require('./resolvers/phenotype-ontology.resolvers'); const pharosResolvers = require('./resolvers/pharos.resolver'); +const stringResolvers = require('./resolvers/string.resolvers'); // Read schema files const clinvarTypeDefs = readFileSync(path.join(__dirname, 'schemas/clinvar.schema.graphql'), 'utf8'); @@ -16,6 +17,7 @@ const geneTypeDefs = readFileSync(path.join(__dirname, 'schemas/gene.schema.grap const dioptTypeDefs = readFileSync(path.join(__dirname, 'schemas/diopt.schema.graphql'), 'utf8'); const phenotypeOntologyTypeDefs = readFileSync(path.join(__dirname, 'schemas/phenotype-ontology.schema.graphql'), 'utf8'); const pharosTypeDefs = readFileSync(path.join(__dirname, 'schemas/pharos.schema.graphql'), 'utf8'); +const stringTypeDefs = readFileSync(path.join(__dirname, 'schemas/string.schema.graphql'), 'utf8'); // Combine all schemas const typeDefs = ` @@ -24,6 +26,7 @@ const typeDefs = ` ${dioptTypeDefs} ${phenotypeOntologyTypeDefs} ${pharosTypeDefs} + ${stringTypeDefs} type Query { clinvarByGeneSymbol(symbol: String!): [Clinvar!]! @@ -52,6 +55,9 @@ const typeDefs = ` pharosTargetById(id: Int!): PharosTarget pharosTargetsByIds(ids: [Int!]!, limit: Int = 100, start: Int = 0): [PharosTarget!]! pharosTargetsByGeneEntrezId(entrezId: Int!, limit: Int = 100, start: Int = 0): [PharosTarget!]! + + stringInteractionsByEnsemblId(ensemblId: String!, limit: Int = 100, start: Int = 0): [StringInteraction!]! + stringInteractionBetweenProteins(ensemblId1: String!, ensemblId2: String!): StringInteraction } `; @@ -76,17 +82,20 @@ const rootValue = { dioptDomainsByEntrezId: dioptResolvers.findDomainsByEntrezId, dioptOrthologsByEntrezId: dioptResolvers.findOrthologsByEntrezId, dioptOrthologsByTaxonId: dioptResolvers.findOrthologsByTaxonIds, - + phenotypeOntologyByPoId: phenotypeOntologyResolvers.findByPoId, phenotypeOntologyByName: phenotypeOntologyResolvers.findByName, phenotypeOntologyByTaxonId: phenotypeOntologyResolvers.findByTaxonId, phenotypeOntologyByNamespace: phenotypeOntologyResolvers.findByNamespace, phenotypeOntologyByCategory: phenotypeOntologyResolvers.findByCategory, - + pharosTargetById: pharosResolvers.pharosTargetById, pharosTargetsByIds: pharosResolvers.pharosTargetsByIds, pharosTargetsByGeneEntrezId: pharosResolvers.pharosTargetsByGeneEntrezId, - + + stringInteractionsByEnsemblProteinId: stringResolvers.stringInteractionsByEnsemblProteinId, + stringInteractionBetweenProteins: stringResolvers.stringInteractionBetweenProteins, + // Type resolvers PharosTarget: pharosResolvers.PharosTarget, }; diff --git a/server/graphql/resolvers/string.resolvers.js b/server/graphql/resolvers/string.resolvers.js new file mode 100644 index 0000000..02d03dd --- /dev/null +++ b/server/graphql/resolvers/string.resolvers.js @@ -0,0 +1,40 @@ +const StringInteractions = require('../../models/string-interactions.model'); + +/** + * String GraphQL resolvers + */ +const stringResolvers = { + stringInteractionsByEnsemblProteinId: async (args) => { + try { + const { ensemblId, limit = 100, start = 0 } = args; + // Find all STRING interactions where ensemblId1 matches the provided ensemblId + const interactions = await StringInteractions + .find({ ensemblId1: ensemblId }) + .skip(start) + .limit(limit); + return interactions; + } catch (error) { + console.error('Error in stringInteractionsByEnsemblId resolver:', error); + throw error; + } + }, + + stringInteractionBetweenProteins: async (args) => { + try { + const { ensemblId1, ensemblId2 } = args; + // Find the specific interaction between two genes (bidirectional search) + const interaction = await StringInteractions.findOne({ ensemblId1, ensemblId2 }); + if (interaction) { + return interaction; + } + const reverseInteraction = await StringInteractions + .findOne({ ensemblId1: ensemblId2, ensemblId2: ensemblId1 }); + return reverseInteraction; + } catch (error) { + console.error('Error in stringInteractionBetweenProteins resolver:', error); + throw error; + } + } +}; + +module.exports = stringResolvers; diff --git a/server/graphql/schemas/string.schema.graphql b/server/graphql/schemas/string.schema.graphql new file mode 100644 index 0000000..9efdaea --- /dev/null +++ b/server/graphql/schemas/string.schema.graphql @@ -0,0 +1,10 @@ +""" +String DB interaction between two genes (experimental, database, and two combined) +""" +type StringInteraction { + ensemblId1: String + ensemblId2: String + experiments: Int + database: Int + combExpDb: Int +} \ No newline at end of file diff --git a/server/models/string-interactions.model.js b/server/models/string-interactions.model.js new file mode 100644 index 0000000..8a7c18c --- /dev/null +++ b/server/models/string-interactions.model.js @@ -0,0 +1,26 @@ +const mongoose = require('mongoose'); + +const stringInteractionSchema = mongoose.Schema({ + ensemblId1: { + type: String, + required: true + }, + ensemblId2: { + type: String, + required: true + }, + experiments: { + type: Number, + required: true + }, + database: { + type: Number, + required: true + }, + combExpDb: { + type: Number, + required: true + } +}, { collection: 'String.12.0' }); + +module.exports = mongoose.model('StringInteractions', stringInteractionSchema); From 212ebc25b0eff106f167495a8393003eea1587ae Mon Sep 17 00:00:00 2001 From: Sun-Young Kim Date: Mon, 3 Nov 2025 13:43:18 -0600 Subject: [PATCH 09/10] fix: lint error --- server/graphql/resolvers/gene.resolvers.js | 6 +++--- server/graphql/resolvers/pharos.resolver.js | 9 --------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/server/graphql/resolvers/gene.resolvers.js b/server/graphql/resolvers/gene.resolvers.js index 18379cd..804c4ab 100644 --- a/server/graphql/resolvers/gene.resolvers.js +++ b/server/graphql/resolvers/gene.resolvers.js @@ -46,7 +46,7 @@ const findByEnsemblId = async ({ ensemblId }) => { { 'xref.ensemblId': ensemblId }, { _id: 0, clinVarIds: 0, dgvIds: 0, geno2mpIds: 0, gos: 0, phenotypes: 0 } ).lean(); - + if (!gene) { return null; } @@ -55,7 +55,7 @@ const findByEnsemblId = async ({ ensemblId }) => { if (gene.alias && typeof gene.alias === 'string') { gene.alias = [gene.alias]; } - + // Handle omimId format if (gene.xref && gene.xref.omimId && gene.xref.omimId.length) { gene.xref.omimId = gene.xref.omimId[0]; @@ -77,7 +77,7 @@ const findByPrefix = async ({ prefix, taxonId, limit = 30 }) => { `^(${prefix.trim().split(/[^a-zA-Z0-9]+/g).join('|')})`, taxonId === 7227 ? '' : 'i' ); - + const genes = await Genes.find( { taxonId, symbol: symbolRegex }, { _id: 0, clinVarIds: 0, gos: 0, dgvIds: 0, geno2mpIds: 0, phenotypes: 0 }, diff --git a/server/graphql/resolvers/pharos.resolver.js b/server/graphql/resolvers/pharos.resolver.js index 3fa46d6..587de5d 100644 --- a/server/graphql/resolvers/pharos.resolver.js +++ b/server/graphql/resolvers/pharos.resolver.js @@ -24,16 +24,13 @@ const pharosResolvers = { pharosTargetsByIds: async (args) => { try { const { ids, limit = 100, start = 0 } = args; - if (ids.length === 0) { return []; } - const targets = await PharosTargets .find({ id: { $in: ids } }) .skip(start) .limit(limit); - return targets; } catch (error) { console.error('Error in pharosTargetsByIds resolver:', error); @@ -44,13 +41,11 @@ const pharosResolvers = { pharosTargetsByGeneEntrezId: async (args) => { try { const { entrezId, limit = 100, start = 0 } = args; - // Use the existing controller logic to get targets by gene const gene = await Genes.findOne({ entrezId }); if (!gene || !gene.pharosTargetIds || gene.pharosTargetIds.length === 0) { return []; } - const targets = await PharosTargets .find({ id: { $in: gene.pharosTargetIds } }) .skip(start) @@ -70,11 +65,9 @@ const pharosResolvers = { if (!parent.drugIds || parent.drugIds.length === 0) { return []; } - const drugs = await PharosDrugs.find({ id: { $in: parent.drugIds } }); - return drugs; } catch (error) { console.error('Error resolving PharosTarget.drugs:', error); @@ -87,11 +80,9 @@ const pharosResolvers = { if (!parent.ligandIds || parent.ligandIds.length === 0) { return []; } - const ligands = await PharosLigands.find({ id: { $in: parent.ligandIds } }); - return ligands; } catch (error) { console.error('Error resolving PharosTarget.ligands:', error); From d3bdddc24d38220dd45b734ac6a06e02fc490b3a Mon Sep 17 00:00:00 2001 From: "Sun-Young (Serena) Kim" Date: Mon, 3 Nov 2025 14:23:39 -0600 Subject: [PATCH 10/10] fix(graphql): remove unused resolver Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/graphql/graphqlHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/graphql/graphqlHandler.js b/server/graphql/graphqlHandler.js index 9d861cb..ad7f386 100644 --- a/server/graphql/graphqlHandler.js +++ b/server/graphql/graphqlHandler.js @@ -81,7 +81,6 @@ const rootValue = { dioptAlignmentByEntrezId: dioptResolvers.findAlignmentsByEntrezId, dioptDomainsByEntrezId: dioptResolvers.findDomainsByEntrezId, dioptOrthologsByEntrezId: dioptResolvers.findOrthologsByEntrezId, - dioptOrthologsByTaxonId: dioptResolvers.findOrthologsByTaxonIds, phenotypeOntologyByPoId: phenotypeOntologyResolvers.findByPoId, phenotypeOntologyByName: phenotypeOntologyResolvers.findByName,