Repository: gitify-app/gitify
Pull request #1085 (2024-05-05 17:15:26)

refactor: extract discussions api calls into client
Extract the discussions GraphQL API call into our api/client, along with the GQL operation document and supporting helpers


Patch:
diff --git a/package.json b/package.json
index 800581ac..24b8e54f 100644
--- a/package.json
+++ b/package.json
@@ -126,9 +126,9 @@
   "devDependencies": {
     "@biomejs/biome": "1.7.2",
     "@electron/notarize": "2.3.0",
-    "@testing-library/react": "15.0.5",
+    "@testing-library/react": "15.0.6",
     "@types/jest": "29.5.12",
-    "@types/node": "20.12.7",
+    "@types/node": "20.12.8",
     "@types/nprogress": "0.2.3",
     "@types/react": "18.3.1",
     "@types/react-router-dom": "5.3.3",
@@ -138,6 +138,7 @@
     "css-loader": "7.1.1",
     "electron": "30.0.1",
     "electron-builder": "24.13.3",
+    "graphql-tag": "2.12.6",
     "husky": "9.0.11",
     "jest": "29.7.0",
     "jest-environment-jsdom": "29.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a886610e..752f4492 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -61,14 +61,14 @@ importers:
         specifier: 2.3.0
         version: 2.3.0
       '@testing-library/react':
-        specifier: 15.0.5
-        version: 15.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 15.0.6
+        version: 15.0.6(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@types/jest':
         specifier: 29.5.12
         version: 29.5.12
       '@types/node':
-        specifier: 20.12.7
-        version: 20.12.7
+        specifier: 20.12.8
+        version: 20.12.8
       '@types/nprogress':
         specifier: 0.2.3
         version: 0.2.3
@@ -96,12 +96,15 @@ importers:
       electron-builder:
         specifier: 24.13.3
         version: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
+      graphql-tag:
+        specifier: 2.12.6
+        version: 2.12.6(graphql@16.8.1)
       husky:
         specifier: 9.0.11
         version: 9.0.11
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+        version: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       jest-environment-jsdom:
         specifier: 29.7.0
         version: 29.7.0
@@ -125,13 +128,13 @@ importers:
         version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4))
       tailwindcss:
         specifier: 3.4.3
-        version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+        version: 3.4.3(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       ts-jest:
         specifier: 29.1.2
-        version: 29.1.2(@babel/core@7.24.3)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)))(typescript@5.4.5)
+        version: 29.1.2(@babel/core@7.24.3)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)))(typescript@5.4.5)
       ts-node:
         specifier: 10.9.2
-        version: 10.9.2(@types/node@20.12.7)(typescript@5.4.5)
+        version: 10.9.2(@types/node@20.12.8)(typescript@5.4.5)
       webpack:
         specifier: 5.91.0
         version: 5.91.0(webpack-cli@5.1.4)
@@ -579,12 +582,16 @@ packages:
     resolution: {integrity: sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==}
     engines: {node: '>=18'}
 
-  '@testing-library/react@15.0.5':
-    resolution: {integrity: sha512-ttodVWYA2i2w4hRa6krKrmS1vKxAEkwDz34y+CwbcrbZUxFzUYN3a5xZyFKo+K6LBseCRCUkwcjATpaNn/UsIA==}
+  '@testing-library/react@15.0.6':
+    resolution: {integrity: sha512-UlbazRtEpQClFOiYp+1BapMT+xyqWMnE+hh9tn5DQ6gmlE7AIZWcGpzZukmDZuFk3By01oiqOf8lRedLS4k6xQ==}
     engines: {node: '>=18'}
     peerDependencies:
+      '@types/react': ^18.0.0
       react: ^18.0.0
       react-dom: ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
 
   '@tootallnate/once@2.0.0':
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
@@ -668,8 +675,8 @@ packages:
   '@types/ms@0.7.34':
     resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
 
-  '@types/node@20.12.7':
-    resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==}
+  '@types/node@20.12.8':
+    resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==}
 
   '@types/nprogress@0.2.3':
     resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
@@ -1650,6 +1657,16 @@ packages:
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
+  graphql-tag@2.12.6:
+    resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
+
+  graphql@16.8.1:
+    resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
+    engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+
   has-flag@3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
@@ -2957,6 +2974,9 @@ packages:
       '@swc/wasm':
         optional: true
 
+  tslib@2.6.2:
+    resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+
   type-detect@4.0.8:
     resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
     engines: {node: '>=4'}
@@ -3510,27 +3530,27 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
       slash: 3.0.0
 
-  '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))':
+  '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))':
     dependencies:
       '@jest/console': 29.7.0
       '@jest/reporters': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.9.0
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      jest-config: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -3555,7 +3575,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -3573,7 +3593,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -3595,7 +3615,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.25
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       collect-v8-coverage: 1.0.2
       exit: 0.1.2
@@ -3665,7 +3685,7 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       '@types/yargs': 17.0.32
       chalk: 4.1.2
 
@@ -3757,13 +3777,15 @@ snapshots:
       lz-string: 1.5.0
       pretty-format: 27.5.1
 
-  '@testing-library/react@15.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@testing-library/react@15.0.6(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
       '@babel/runtime': 7.24.1
       '@testing-library/dom': 10.0.0
       '@types/react-dom': 18.2.22
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
+    optionalDependencies:
+      '@types/react': 18.3.1
 
   '@tootallnate/once@2.0.0': {}
 
@@ -3802,7 +3824,7 @@ snapshots:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       '@types/responselike': 1.0.3
 
   '@types/debug@4.1.12':
@@ -3823,11 +3845,11 @@ snapshots:
 
   '@types/fs-extra@9.0.13':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
 
   '@types/graceful-fs@4.1.9':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
 
   '@types/history@4.7.11': {}
 
@@ -3850,7 +3872,7 @@ snapshots:
 
   '@types/jsdom@20.0.1':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       '@types/tough-cookie': 4.0.5
       parse5: 7.1.2
 
@@ -3858,11 +3880,11 @@ snapshots:
 
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
 
   '@types/ms@0.7.34': {}
 
-  '@types/node@20.12.7':
+  '@types/node@20.12.8':
     dependencies:
       undici-types: 5.26.5
 
@@ -3870,7 +3892,7 @@ snapshots:
 
   '@types/plist@3.0.5':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       xmlbuilder: 15.1.1
     optional: true
 
@@ -3906,7 +3928,7 @@ snapshots:
 
   '@types/responselike@1.0.3':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
 
   '@types/stack-utils@2.0.3': {}
 
@@ -3923,7 +3945,7 @@ snapshots:
 
   '@types/yauzl@2.10.3':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
     optional: true
 
   '@webassemblyjs/ast@1.12.1':
@@ -4500,13 +4522,13 @@ snapshots:
       buffer: 5.7.1
     optional: true
 
-  create-jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  create-jest@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
       '@jest/types': 29.6.3
       chalk: 4.1.2
       exit: 0.1.2
       graceful-fs: 4.2.11
-      jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      jest-config: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       jest-util: 29.7.0
       prompts: 2.4.2
     transitivePeerDependencies:
@@ -4714,7 +4736,7 @@ snapshots:
   electron@30.0.1:
     dependencies:
       '@electron/get': 2.0.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       extract-zip: 2.0.1
     transitivePeerDependencies:
       - supports-color
@@ -5008,6 +5030,13 @@ snapshots:
 
   graceful-fs@4.2.11: {}
 
+  graphql-tag@2.12.6(graphql@16.8.1):
+    dependencies:
+      graphql: 16.8.1
+      tslib: 2.6.2
+
+  graphql@16.8.1: {}
+
   has-flag@3.0.0: {}
 
   has-flag@4.0.0: {}
@@ -5210,7 +5239,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.1
@@ -5230,16 +5259,16 @@ snapshots:
       - babel-plugin-macros
       - supports-color
 
-  jest-cli@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  jest-cli@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
-      '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
       chalk: 4.1.2
-      create-jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      create-jest: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       exit: 0.1.2
       import-local: 3.1.0
-      jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      jest-config: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       jest-util: 29.7.0
       jest-validate: 29.7.0
       yargs: 17.7.2
@@ -5249,7 +5278,7 @@ snapshots:
       - supports-color
       - ts-node
 
-  jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  jest-config@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
       '@babel/core': 7.24.3
       '@jest/test-sequencer': 29.7.0
@@ -5274,8 +5303,8 @@ snapshots:
       slash: 3.0.0
       strip-json-comments: 3.1.1
     optionalDependencies:
-      '@types/node': 20.12.7
-      ts-node: 10.9.2(@types/node@20.12.7)(typescript@5.4.5)
+      '@types/node': 20.12.8
+      ts-node: 10.9.2(@types/node@20.12.8)(typescript@5.4.5)
     transitivePeerDependencies:
       - babel-plugin-macros
       - supports-color
@@ -5305,7 +5334,7 @@ snapshots:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
       '@types/jsdom': 20.0.1
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-mock: 29.7.0
       jest-util: 29.7.0
       jsdom: 20.0.3
@@ -5319,7 +5348,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -5329,7 +5358,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.9
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -5368,7 +5397,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -5403,7 +5432,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -5431,7 +5460,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       cjs-module-lexer: 1.2.3
       collect-v8-coverage: 1.0.2
@@ -5477,7 +5506,7 @@ snapshots:
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       chalk: 4.1.2
       ci-info: 3.9.0
       graceful-fs: 4.2.11
@@ -5496,7 +5525,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -5505,23 +5534,23 @@ snapshots:
 
   jest-worker@27.5.1:
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
-  jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  jest@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
-      '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       '@jest/types': 29.6.3
       import-local: 3.1.0
-      jest-cli: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      jest-cli: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
     transitivePeerDependencies:
       - '@types/node'
       - babel-plugin-macros
@@ -5866,13 +5895,13 @@ snapshots:
       camelcase-css: 2.0.1
       postcss: 8.4.38
 
-  postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
       lilconfig: 3.1.1
       yaml: 2.4.1
     optionalDependencies:
       postcss: 8.4.38
-      ts-node: 10.9.2(@types/node@20.12.7)(typescript@5.4.5)
+      ts-node: 10.9.2(@types/node@20.12.8)(typescript@5.4.5)
 
   postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.91.0(webpack-cli@5.1.4)):
     dependencies:
@@ -6308,7 +6337,7 @@ snapshots:
 
   symbol-tree@3.2.4: {}
 
-  tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)):
+  tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)):
     dependencies:
       '@alloc/quick-lru': 5.2.0
       arg: 5.0.2
@@ -6327,7 +6356,7 @@ snapshots:
       postcss: 8.4.38
       postcss-import: 15.1.0(postcss@8.4.38)
       postcss-js: 4.0.1(postcss@8.4.38)
-      postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       postcss-nested: 6.0.1(postcss@8.4.38)
       postcss-selector-parser: 6.0.16
       resolve: 1.22.8
@@ -6422,11 +6451,11 @@ snapshots:
 
   ts-interface-checker@0.1.13: {}
 
-  ts-jest@29.1.2(@babel/core@7.24.3)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)))(typescript@5.4.5):
+  ts-jest@29.1.2(@babel/core@7.24.3)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5)))(typescript@5.4.5):
     dependencies:
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
-      jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))
+      jest: 29.7.0(@types/node@20.12.8)(ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5))
       jest-util: 29.7.0
       json5: 2.2.3
       lodash.memoize: 4.1.2
@@ -6449,14 +6478,14 @@ snapshots:
       typescript: 5.4.5
       webpack: 5.91.0(webpack-cli@5.1.4)
 
-  ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5):
+  ts-node@10.9.2(@types/node@20.12.8)(typescript@5.4.5):
     dependencies:
       '@cspotcode/source-map-support': 0.8.1
       '@tsconfig/node10': 1.0.11
       '@tsconfig/node12': 1.0.11
       '@tsconfig/node14': 1.0.3
       '@tsconfig/node16': 1.0.4
-      '@types/node': 20.12.7
+      '@types/node': 20.12.8
       acorn: 8.11.3
       acorn-walk: 8.3.2
       arg: 4.1.3
@@ -6467,6 +6496,8 @@ snapshots:
       v8-compile-cache-lib: 3.0.1
       yn: 3.1.1
 
+  tslib@2.6.2: {}
+
   type-detect@4.0.8: {}
 
   type-fest@0.13.1:
diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts
index e7a4dad1..51f1e514 100644
--- a/src/__mocks__/mockedData.ts
+++ b/src/__mocks__/mockedData.ts
@@ -473,41 +473,37 @@ export const mockDiscussionComments: DiscussionComments = {
 
 export const mockedGraphQLResponse: GraphQLSearch<Discussion> = {
   data: {
-    data: {
-      search: {
-        nodes: [
-          {
-            viewerSubscription: 'SUBSCRIBED',
-            title: '1.16.0',
-            isAnswered: false,
-            stateReason: 'OPEN',
-            url: 'https://github.com/gitify-app/notifications-test/discussions/612',
-            author: {
-              login: 'discussion-creator',
-              url: 'https://github.com/discussion-creator',
-              avatar_url:
-                'https://avatars.githubusercontent.com/u/123456789?v=4',
-              type: 'User',
-            },
-            comments: mockDiscussionComments,
+    search: {
+      nodes: [
+        {
+          viewerSubscription: 'SUBSCRIBED',
+          title: '1.16.0',
+          isAnswered: false,
+          stateReason: 'OPEN',
+          url: 'https://github.com/gitify-app/notifications-test/discussions/612',
+          author: {
+            login: 'discussion-creator',
+            url: 'https://github.com/discussion-creator',
+            avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4',
+            type: 'User',
           },
-          {
-            viewerSubscription: 'IGNORED',
-            title: '1.16.0',
-            isAnswered: false,
-            stateReason: 'ANSWERED',
-            url: 'https://github.com/gitify-app/notifications-test/discussions/123',
-            author: {
-              login: 'discussion-creator',
-              url: 'https://github.com/discussion-creator',
-              avatar_url:
-                'https://avatars.githubusercontent.com/u/123456789?v=4',
-              type: 'User',
-            },
-            comments: mockDiscussionComments,
+          comments: mockDiscussionComments,
+        },
+        {
+          viewerSubscription: 'IGNORED',
+          title: '1.16.0',
+          isAnswered: false,
+          stateReason: 'ANSWERED',
+          url: 'https://github.com/gitify-app/notifications-test/discussions/123',
+          author: {
+            login: 'discussion-creator',
+            url: 'https://github.com/discussion-creator',
+            avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4',
+            type: 'User',
           },
-        ],
-      },
+          comments: mockDiscussionComments,
+        },
+      ],
     },
   },
 };
diff --git a/src/__mocks__/partial-mocks.ts b/src/__mocks__/partial-mocks.ts
index 2c8b52b7..f0d2c0ef 100644
--- a/src/__mocks__/partial-mocks.ts
+++ b/src/__mocks__/partial-mocks.ts
@@ -1,9 +1,11 @@
 import type { Notification, Subject, User } from '../typesGitHub';
+import Constants from '../utils/constants';
 
 export function partialMockNotification(
   subject: Partial<Subject>,
 ): Notification {
   const mockNotification: Partial<Notification> = {
+    hostname: Constants.GITHUB_API_BASE_URL,
     subject: subject as Subject,
   };
 
diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts
index 19fce056..3b5509bb 100644
--- a/src/typesGitHub.ts
+++ b/src/typesGitHub.ts
@@ -407,10 +407,8 @@ export interface Release {
 
 export interface GraphQLSearch<T> {
   data: {
-    data: {
-      search: {
-        nodes: T[];
-      };
+    search: {
+      nodes: T[];
     };
   };
 }
diff --git a/src/utils/api/client.ts b/src/utils/api/client.ts
index 0849f7e7..27c90bb5 100644
--- a/src/utils/api/client.ts
+++ b/src/utils/api/client.ts
@@ -3,6 +3,8 @@ import type { SettingsState } from '../../types';
 import type {
   Commit,
   CommitComment,
+  Discussion,
+  GraphQLSearch,
   Issue,
   IssueOrPullRequestComment,
   Notification,
@@ -15,6 +17,11 @@ import type {
 import { getGitHubAPIBaseUrl } from '../helpers';
 import { apiRequestAuth } from './request';
 
+import { print } from 'graphql/language/printer';
+import Constants from '../constants';
+import { QUERY_SEARCH_DISCUSSIONS } from './graphql/discussions';
+import { formatSearchQueryString } from './utils';
+
 /**
  * Get Hypermedia links to resources accessible in GitHub's REST API
  *
@@ -213,3 +220,28 @@ export async function getHtmlUrl(url: string, token: string): Promise<string> {
     console.error('Failed to get html url');
   }
 }
+
+/**
+ * Search for Discussions that match notification title and repository.
+ *
+ * Returns first 10 matching discussions and their latest comments / replies
+ *
+ */
+export async function searchDiscussions(
+  notification: Notification,
+  token: string,
+): AxiosPromise<GraphQLSearch<Discussion>> {
+  return apiRequestAuth(Constants.GITHUB_API_GRAPHQL_URL, 'POST', token, {
+    query: print(QUERY_SEARCH_DISCUSSIONS),
+    variables: {
+      queryStatement: formatSearchQueryString(
+        notification.repository.full_name,
+        notification.subject.title,
+        notification.updated_at,
+      ),
+      firstDiscussions: 10,
+      lastComments: 100,
+      lastReplies: 1,
+    },
+  });
+}
diff --git a/src/utils/api/graphql/discussions.ts b/src/utils/api/graphql/discussions.ts
new file mode 100644
index 00000000..3679367d
--- /dev/null
+++ b/src/utils/api/graphql/discussions.ts
@@ -0,0 +1,59 @@
+import gql from 'graphql-tag';
+
+const FRAGMENT_AUTHOR = gql`
+  fragment AuthorFields on Actor {
+    login
+    url
+    avatar_url: avatarUrl
+    type: __typename
+  }
+`;
+
+const FRAGMENT_COMMENTS = gql`
+  fragment CommentFields on DiscussionComment {
+    databaseId
+    createdAt
+    author {
+      ...AuthorFields
+    }
+  }
+
+  ${FRAGMENT_AUTHOR}
+`;
+
+export const QUERY_SEARCH_DISCUSSIONS = gql`
+  query fetchDiscussions(
+    $queryStatement: String!,
+    $firstDiscussions: Int,
+    $lastComments: Int,
+    $lastReplies: Int
+  ) {
+    search(query:$queryStatement, type: DISCUSSION, first: $firstDiscussions) {
+      nodes {
+        ... on Discussion {
+          viewerSubscription
+          title
+          stateReason
+          isAnswered
+          url
+          author {
+            ...AuthorFields
+          }
+          comments(last: $lastComments){
+            nodes {
+              ...CommentFields
+              replies(last: $lastReplies) {
+                nodes {
+                  ...CommentFields
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  ${FRAGMENT_AUTHOR}
+  ${FRAGMENT_COMMENTS}
+`;
diff --git a/src/utils/api/utils.ts b/src/utils/api/utils.ts
new file mode 100644
index 00000000..6a833c9f
--- /dev/null
+++ b/src/utils/api/utils.ts
@@ -0,0 +1,11 @@
+export function formatSearchQueryString(
+  repo: string,
+  title: string,
+  lastUpdated: string,
+): string {
+  return `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`;
+}
+
+export function addHours(date: string, hours: number): string {
+  return new Date(new Date(date).getTime() + hours * 36e5).toISOString();
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 24d8eb0b..e41c0775 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -11,6 +11,7 @@ export const Constants = {
   },
 
   GITHUB_API_BASE_URL: 'https://api.github.com',
+  GITHUB_API_GRAPHQL_URL: 'https://api.github.com/graphql',
 
   REPO_SLUG: 'gitify-app/gitify',
 
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index e84a7887..575e6645 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -2,12 +2,10 @@ import type { AuthState } from '../types';
 import type {
   Discussion,
   DiscussionComment,
-  GraphQLSearch,
   Notification,
 } from '../typesGitHub';
 import { openExternalLink } from '../utils/comms';
-import { getHtmlUrl } from './api/client';
-import { apiRequestAuth } from './api/request';
+import { getHtmlUrl, searchDiscussions } from './api/client';
 import { Constants } from './constants';
 import { getCheckSuiteAttributes, getWorkflowRunAttributes } from './subject';
 
@@ -62,18 +60,6 @@ export function generateNotificationReferrerId(
   return buffer.toString('base64');
 }
 
-export function addHours(date: string, hours: number): string {
-  return new Date(new Date(date).getTime() + hours * 36e5).toISOString();
-}
-
-export function formatSearchQueryString(
-  repo: string,
-  title: string,
-  lastUpdated: string,
-): string {
-  return `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`;
-}
-
 export function getCheckSuiteUrl(notification: Notification) {
   let url = `${notification.repository.html_url}/actions`;
   const filters = [];
@@ -145,85 +131,21 @@ export async function fetchDiscussion(
   notification: Notification,
   token: string,
 ): Promise<Discussion | null> {
-  const response: GraphQLSearch<Discussion> = await apiRequestAuth(
-    'https://api.github.com/graphql',
-    'POST',
-    token,
-    {
-      query: `
-        fragment AuthorFields on Actor {
-          login
-          url
-          avatar_url: avatarUrl
-          type: __typename
-        }
-      
-        fragment CommentFields on DiscussionComment {
-          databaseId
-          createdAt
-          author {
-            ...AuthorFields
-          }
-        }
-      
-        query fetchDiscussions(
-          $queryStatement: String!,
-          $type: SearchType!,
-          $firstDiscussions: Int,
-          $lastComments: Int,
-          $firstReplies: Int
-        ) {
-          search(query:$queryStatement, type: $type, first: $firstDiscussions) {
-            nodes {
-              ... on Discussion {
-                viewerSubscription
-                title
-                stateReason
-                isAnswered
-                url
-                author {
-                  ...AuthorFields
-                }
-                comments(last: $lastComments){
-                  nodes {
-                    ...CommentFields
-                    replies(last: $firstReplies) {
-                      nodes {
-                        ...CommentFields
-                      }
-                    }
-                  }
-                }
-              }
-            }
-          }
-        }
-      `,
-      variables: {
-        queryStatement: formatSearchQueryString(
-          notification.repository.full_name,
-          notification.subject.title,
-          notification.updated_at,
-        ),
-        type: 'DISCUSSION',
-        firstDiscussions: 10,
-        lastComments: 100,
-        firstReplies: 1,
-      },
-    },
-  );
+  try {
+    const response = await searchDiscussions(notification, token);
 
-  let discussions =
-    response?.data?.data.search.nodes.filter(
-      (discussion) => discussion.title === notification.subject.title,
-    ) || [];
+    let discussions =
+      response.data?.data.search.nodes.filter(
+        (discussion) => discussion.title === notification.subject.title,
+      ) || [];
 
-  if (discussions.length > 1)
-    discussions = discussions.filter(
-      (discussion) => discussion.viewerSubscription === 'SUBSCRIBED',
-    );
+    if (discussions.length > 1)
+      discussions = discussions.filter(
+        (discussion) => discussion.viewerSubscription === 'SUBSCRIBED',
+      );
 
-  return discussions[0];
+    return discussions[0];
+  } catch (err) {}
 }
 
 export function getLatestDiscussionComment(


Test patch:
diff --git a/src/utils/api/utils.test.ts b/src/utils/api/utils.test.ts
new file mode 100644
index 00000000..001b3f3c
--- /dev/null
+++ b/src/utils/api/utils.test.ts
@@ -0,0 +1,29 @@
+import { addHours, formatSearchQueryString } from './utils';
+
+describe('utils/api/utils.ts', () => {
+  describe('addHours', () => {
+    test('adds hours correctly for positive values', () => {
+      const result = addHours('2024-02-20T12:00:00.000Z', 3);
+      expect(result).toBe('2024-02-20T15:00:00.000Z');
+    });
+
+    test('adds hours correctly for negative values', () => {
+      const result = addHours('2024-02-20T12:00:00.000Z', -2);
+      expect(result).toBe('2024-02-20T10:00:00.000Z');
+    });
+  });
+
+  describe('formatSearchQueryString', () => {
+    test('formats search query string correctly', () => {
+      const result = formatSearchQueryString(
+        'exampleRepo',
+        'exampleTitle',
+        '2024-02-20T12:00:00.000Z',
+      );
+
+      expect(result).toBe(
+        'exampleTitle in:title repo:exampleRepo updated:>2024-02-20T10:00:00.000Z',
+      );
+    });
+  });
+});
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index ca7fad00..d80ab645 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -8,10 +8,8 @@ import {
 import type { SubjectType } from '../typesGitHub';
 import * as apiRequests from './api/request';
 import {
-  addHours,
   addNotificationReferrerIdToUrl,
   formatForDisplay,
-  formatSearchQueryString,
   generateGitHubWebUrl,
   generateNotificationReferrerId,
   getGitHubAPIBaseUrl,
@@ -102,33 +100,6 @@ describe('utils/helpers.ts', () => {
     });
   });
 
-  describe('addHours', () => {
-    // Example test using Jest
-    test('adds hours correctly for positive values', () => {
-      const result = addHours('2024-02-20T12:00:00.000Z', 3);
-      expect(result).toBe('2024-02-20T15:00:00.000Z');
-    });
-
-    test('adds hours correctly for negative values', () => {
-      const result = addHours('2024-02-20T12:00:00.000Z', -2);
-      expect(result).toBe('2024-02-20T10:00:00.000Z');
-    });
-  });
-
-  describe('formatSearchQueryString', () => {
-    test('formats search query string correctly', () => {
-      const result = formatSearchQueryString(
-        'exampleRepo',
-        'exampleTitle',
-        '2024-02-20T12:00:00.000Z',
-      );
-
-      expect(result).toBe(
-        'exampleTitle in:title repo:exampleRepo updated:>2024-02-20T10:00:00.000Z',
-      );
-    });
-  });
-
   describe('generateGitHubWebUrl', () => {
     const mockedHtmlUrl =
       'https://github.com/gitify-app/notifications-test/issues/785';
@@ -398,7 +369,7 @@ describe('utils/helpers.ts', () => {
         );
       });
 
-      it('when no subject urls and no discussions found via query, default to linking to repository discussions', async () => {
+      it('link to matching discussion and comment hash', async () => {
         const subject = {
           title: '1.16.0',
           url: null,
@@ -407,7 +378,11 @@ describe('utils/helpers.ts', () => {
         };
 
         const requestPromise = new Promise((resolve) =>
-          resolve(mockedGraphQLResponse as AxiosResponse),
+          resolve({
+            data: {
+              ...mockedGraphQLResponse,
+            },
+          } as AxiosResponse),
         ) as AxiosPromise;
 
         apiRequestAuthMock.mockResolvedValue(requestPromise);