• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-line-bot / 13899815274

17 Mar 2025 12:47PM UTC coverage: 90.688% (-0.07%) from 90.761%
13899815274

Pull #404

github

MrOrz
fix: gql response type
Pull Request #404: feat: throw error when GraphQL server do not return correct batched response

544 of 645 branches covered (84.34%)

Branch coverage included in aggregate %.

3 of 4 new or added lines in 1 file covered. (75.0%)

1131 of 1202 relevant lines covered (94.09%)

12.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.02
/src/lib/gql.ts
1
import fetch from 'node-fetch';
2
import rollbar from './rollbar';
3
import { format } from 'url';
4
import Dataloader from 'dataloader';
5

6
const API_URL = process.env.API_URL || 'https://dev-api.cofacts.tw/graphql';
16!
7

8
type QV = {
9
  query: string;
10
  variables?: object;
11
};
12

13
type Resp = { data: object | null; errors: { message: string }[] | undefined };
14

15
// Maps URL to dataloader. Cleared after batched request is fired.
16
// Exported just for unit test.
17
export const loaders: Record<string, Dataloader<QV, Resp>> = {};
16✔
18

19
/**
20
 * Returns a dataloader instance that can send query & variable to the GraphQL endpoint specified by `url`.
21
 *
22
 * The dataloader instance is automatically created when not exist for the specified `url`, and is
23
 * cleared automatically when the batch request fires.
24
 *
25
 * @param url - GraphQL endpoint URL
26
 * @returns A dataloader instance that loads response of the given {query, variable}
27
 */
28
function getGraphQLRespLoader(url: string) {
29
  if (loaders[url]) return loaders[url];
6✔
30

31
  return (loaders[url] = new Dataloader<QV, Resp>(async (queryAndVariables) => {
5✔
32
    // Clear dataloader so that next batch will get a fresh dataloader
33
    delete loaders[url];
5✔
34

35
    let resp;
36
    try {
5✔
37
      resp = await fetch(url, {
5✔
38
        method: 'POST',
39
        headers: {
40
          'Content-Type': 'application/json',
41
          'x-app-secret': process.env.APP_SECRET ?? '',
5!
42
        },
43
        // Implements Apollo's transport layer batching
44
        // https://www.apollographql.com/blog/apollo-client/performance/query-batching/#1bce
45
        //
46
        body: JSON.stringify(queryAndVariables),
47
      });
48
      const responses: Resp[] | Resp /* Can be singlular on error */ =
49
        await resp.json();
5✔
50
      if (
5!
51
        !Array.isArray(responses) ||
10✔
52
        responses.length !== queryAndVariables.length
53
      )
NEW
54
        throw new Error(`Invalid batch response ${JSON.stringify(responses)}`);
×
55
      return responses;
5✔
56
    } catch (error) {
57
      console.error(`Failed to fetch GraphQL response from ${url}:`, {
×
58
        status: resp?.status,
59
        statusText: resp?.statusText,
60
        headers: resp?.headers
×
61
          ? JSON.stringify(Object.fromEntries(resp.headers.entries()))
62
          : undefined,
63
        error,
64
      });
65
      throw error;
×
66
    }
67
  }));
68
}
69

70
// Usage:
71
//
72
// import gql from './util/GraphQL';
73
// gql`query($var: Type) { foo }`({var: 123}).then(...)
74
//
75
// gql`...`() returns a promise that resolves to immutable Map({data, errors}).
76
//
77
// We use template string here so that Atom's language-babel does syntax highlight
78
// for us.
79
//
80
export default (query: TemplateStringsArray, ...substitutions: string[]) =>
81
  <QueryResp extends object, Variable>(
6✔
82
    variables: Variable,
83
    search?: Record<string, string | number>
84
  ) => {
85
    const queryAndVariable: QV = {
6✔
86
      query: String.raw(query, ...substitutions),
87
    };
88

89
    if (variables) queryAndVariable.variables = variables;
6✔
90

91
    const URL = `${API_URL}${format({ query: search })}`;
6✔
92

93
    return getGraphQLRespLoader(URL)
6✔
94
      .load(queryAndVariable)
95
      .then((resp) => {
96
        // We cannot get status code of individual request in transport layer batching.
97
        // but we can guess that it's not 2xx if `data` is null or does not exist.
98
        // Ref: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#status-codes
99
        //
100
        if (!('data' in resp) || !resp.data || typeof resp.data !== 'object') {
6✔
101
          const errorStr =
102
            'errors' in resp && Array.isArray(resp.errors)
1!
103
              ? resp.errors.map(({ message }) => message).join('\n')
1✔
104
              : 'Unknown error';
105

106
          throw new Error(`GraphQL Error: ${errorStr}`);
1✔
107
        }
108

109
        if ('errors' in resp && resp.errors) {
5✔
110
          console.error('GraphQL operation contains error:', resp.errors);
1✔
111
          rollbar.error(
1✔
112
            'GraphQL error',
113
            {
114
              body: JSON.stringify(queryAndVariable),
115
              url: URL,
116
            },
117
            { resp }
118
          );
119
        }
120

121
        return resp as GqlResponse<QueryResp>;
5✔
122
      });
123
  };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc