Application - Client - Angular & GraphQL

9/18/2021 AngularGraphQL

Để giao tiếp với server sử dụng Apollo Server, client có thể sử dụng Apollo Client. Với angular có thể sử dụng Apollo Angular.

# Install:

Install theo hướng dẫn sau https://apollo-angular.com/docs/get-started (opens new window)

# GraqhQL Module:

...
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { ApolloLink } from 'apollo-link';

const UNAUTHENTICATED_CODE = 'UNAUTHENTICATED';

@NgModule({
  exports: [
    ApolloModule,
    HttpLinkModule,
  ],
})
export class GraphQLModule {
  constructor(
    apollo: Apollo,
    httpLink: HttpLink,
    public authService: AuthenticationService,
    public notificationService: NotificationService,
  ) {

    const http = httpLink.create({uri: `${environment.apiUrl}/graphql`});
    const authLink = new ApolloLink((operation, forward) => {
      if (this.isExceptedOperator(operation.operationName)) {
        operation.setContext(({headers}) => ({
          headers: {
            token: this.authService.getToken(),
            ...headers
          }
        }));
      }
      return forward(operation);
    });
        
    const subscriptionClient = new SubscriptionClient(
      `${environment.webSocketUrl}/graphql`,
      {
        reconnect: true,
        connectionParams: {
          authToken: this.authService.getToken(),
        },
      },
    );

    const wsLink = new WebSocketLink(subscriptionClient);

    const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
      if (graphQLErrors) {
        for (const err of graphQLErrors) {
          const code = err.extensions.code;
          if (code && code === UNAUTHENTICATED_CODE && this.isExceptedOperator(operation.operationName)) {
            const oldHeaders = operation.getContext().headers;
            // Retry the request
            return promiseToObservable(this.getRefreshToken())
              .flatMap(
                (res: any) => {
                  const data: RefreshTokenMutation = res.data;
                  if (data.refreshToken && data.refreshToken.token) {
                    const accessToken = res.data.refreshToken.token;
                    this.authService.setToken(accessToken);
                    operation.setContext({
                      headers: {
                        ...oldHeaders,
                        token: accessToken
                      },
                    });
                    return forward(operation)
                  } else {
                    this.authService.logout();
                    window.location.reload();
                  }
                }
              );
          } else {
            this.notificationService.openSnackBar(err.message);
          }
        }
      }

      if (networkError) {
        console.log('networkError', networkError);
      }
    });
   

    const httpLinkWithErrorHandling = ApolloLink.from([
      errorLink,
      authLink.concat(http),
    ]);

    // using the ability to split links, you can send data to each link
    // depending on what kind of operation is being sent
    const link = split(
      // split based on operation type
      ({query}) => {
        const {kind, operation} = getMainDefinition(query);
        return kind === 'OperationDefinition' && operation === 'subscription';
      },
      wsLink,
      httpLinkWithErrorHandling,
    );

    apollo.create({
      // By default, this client will send queries to the
      // `/graphql` endpoint on the same host
      link,
      cache: new InMemoryCache({
        addTypename: false
      })
    });
  }

  getRefreshToken(): Promise<any> {
    return this.authService
      .refreshToken()
      .pipe(first())
      .toPromise();
  }

  isExceptedOperator(operationName: string) {
    return !['loginMutation', 'refreshTokenMutation'].includes(operationName)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  • Http Link: Gửi GraphQL operation đến server. Trường hợp muốn set token vào headers trước khi gửi request thì thêm authLink.

    authLink.concat(http),
    
    1
  • errorLink: Handle trong trường hợp access token hết hạn, sau khi request 1 token mới sẽ retry lại request ban đầu. Để có thể retry được request cần return forward(operation) tại functiononError(). Nhưng getRefreshToken đang trả về Observable nên mình có dùng thêm function promiseToObservable

    export default promise =>
      new Observable((subscriber: any) => {
        promise.then(
          (value) => {
            if (subscriber.closed) return;
            subscriber.next(value);
            subscriber.complete();
          },
          err => subscriber.error(err)
        );
        return subscriber;
      });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    Version rxjs hiện đang là 6.6.7, khuyến nghi nên dùng lastvaluefrom đối với rxjs ^7

  • wsLink: Thực thi subscription qua WebSocket. Thêm authToken trong connectionParams để verify client.


Series:

GitHub: https://github.com/ninhnguyen22/nestjs-angular-graphql (opens new window)