I'm trying to create a logging interceptor with graphql, apollo federation and nestjs. I'm following this guide and modified my code to
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '#nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
#Injectable()
export class LoggingInterceptor implements NestInterceptor {
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
And apply it like this:
const app: INestApplication = await NestFactory.create(AppModule, new ExpressAdapter(expressApp), {
logger,
});
app.useGlobalInterceptors(new LoggingInterceptor());
In tcpdump an http request is present between apollo-federation and this service when I post graphql request to federation. Despite it there're no logs from my interceptor. But If i make a direct graphql request to this ms (w/o federation), logs are there.
Also another question, I would like to catch response headers and string body. I think this could be done on express level only, but it somehow doesn't work at all, I tried everything in this answer.
In order to make interceptors work on federated field resolvers you need to enable them when configuring GraphQLFederationModule using fieldResolverEnhancers property:
GraphQLFederationModule.forRoot({
...
fieldResolverEnhancers: ['interceptors'],
}),
Related
Good morning,
I have an Angular WebApp that uses GraphQL codegen with (apollo-angular plugin and all the typescript plugins). Everything works fine but I want to handle Hasura Roles and Hasura User ID. From Hasura Console everything is configured correctly and working.
Only thing I am missing is how to handle this on the front end. I need to add X-Hasura-Role and X-Hasura-User-Id headers to every request sent to Hasura.
Is there a way to do this with graphql-codegen?
What is the right way to do this?
I know I can add the headers section on the codegen.yml, but obviously the role and userid are dynamic so I cannot hardcode anything there.
Should I use maybe a customFetch component? This component, thought, should only intercept every request sent to Hasura and add the headers needed. I have no idea how to do this so I hope you can help me (I also hope there is a better solution)
Best regards
When you create your Apollo client instance in the Angular application you can set it up to pass along the Authorization header which should contain the user's id and their roles.
There are examples of this in the Angular Apollo docs. Eg:
import { NgModule } from '#angular/core';
import { HttpClientModule } from '#angular/common/http';
import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache,ApolloLink } from '#apollo/client/core';
import { setContext } from '#apollo/client/link/context';
const uri = '/graphql';
export function createApollo(httpLink: HttpLink) {
const basic = setContext((operation, context) => ({
headers: {
Accept: 'charset=utf-8'
}
}));
const auth = setContext((operation, context) => {
const token = localStorage.getItem('token');
if (token === null) {
return {};
} else {
return {
headers: {
Authorization: `JWT ${token}`
}
};
}
});
const link = ApolloLink.from([basic, auth, httpLink.create({ uri })]);
const cache = new InMemoryCache();
return {
link,
cache
}
}
#NgModule({
exports: [
HttpClientModule,
],
providers: [{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink]
}]
})
export class GraphQLModule {}
It is up to you to ensure that the JWT token that will be passed along with your request is available in the front end. Ultimately you're going to have to implement some kind of authentication approach to allow the user to sign in and pass the token to your front end application.
More information is available in the Hasura Docs for Authentication
There are also a number of tutorials and guides for integrating with different third party auth providers
From Java standpoint using Exceptions to handle EXPECTED outcomes is wrong (Exceptions should be what they are called).
For all my services I've created wrapper mechanism that basically gives details on failure if such happens (all returns are arbitrary Result<?>). Now I need to display this message on client browser in some popup.
With Angular there is HttpClient that actually supports catching http response errors:
https://angular.io/guide/http#error-handling
Is this a viable way of reporting error from server to client?
Is there a way in Angular to define some extractor that would split responses from backend API?
Say I'd make my whole REST API return bodies:
{
messageType: "", // Success, Failure, Warning
message: "Message",
content: {
...
}
}
And that way I could strip message, messageType in interceptor, display them in popup, and pass only content further as body?
A good way to capture all exceptions at service side using #ControllerAdvice and throw the user/feature specific exceptions with the expected exception message and status code in standard structure so that front end can have an exception controller to evaluate this exception message on a fly in popup message dynamic to any message from service.
Since I came to web from pure Java backends - Exceptions in JAVA are bad (generally speaking), why follow same bad practices (using errors as info) when using HTTP?
After some more reading I can say - don't unless its actually an error, e.g:
404 if client tries to access entity with ID that is not there.
500 when server ACTUALLY can't handle something (actual Exception).
Here is Angular part for my Result<?> system.
Interceptor:
import { Injectable } from '#angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Globals } from '../globals.service';
#Injectable()
export class PopupInterceptor implements HttpInterceptor {
constructor(private globals: Globals) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.startsWith(this.globals.apiHost)) {
return next.handle(req)
.pipe(
map(e => {
if (e instanceof HttpResponse) {
const response = <Response>e.body;
// use response to do whatever - I am injecting popup service and pushing response.message there for display in component.
return e.clone({ body: response.result });
}
})
);
}
return next.handle(req);
}
}
interface Response {
type: string;
message: string;
result: any;
}
globals.service:
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class Globals {
apiHost = '/api/';
}
app.module:
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: PopupInterceptor, multi: true }
],
AppSync uses MQTT over WebSockets for its subscription, yet Apollo uses WebSockets. Neither Subscription component or subscribeForMore in Query component works for me when using apollo with AppSync.
One AppSync feature that generated a lot of buzz is its emphasis on
real-time data. Under the hood, AppSync’s real-time feature is powered
by GraphQL subscriptions. While Apollo bases its subscriptions on
WebSockets via subscriptions-transport-ws, subscriptions in GraphQL
are actually flexible enough for you to base them on another messaging
technology. Instead of WebSockets, AppSync’s subscriptions use MQTT as
the transport layer.
Is there any way to make use of AppSync while still using Apollo?
Ok, here is how it worked for me. You'll need to use aws-appsync SDK (https://github.com/awslabs/aws-mobile-appsync-sdk-js) to use Apollo with AppSync. Didn't have to make any other change to make subscription work with AppSync.
Configure ApolloProvider and client:
// App.js
import React from 'react';
import { Platform, StatusBar, StyleSheet, View } from 'react-native';
import { AppLoading, Asset, Font, Icon } from 'expo';
import AWSAppSyncClient from 'aws-appsync' // <--------- use this instead of Apollo Client
import {ApolloProvider} from 'react-apollo'
import { Rehydrated } from 'aws-appsync-react' // <--------- Rehydrated is required to work with Apollo
import config from './aws-exports'
import { SERVER_ENDPOINT, CHAIN_ID } from 'react-native-dotenv'
import AppNavigator from './navigation/AppNavigator';
const client = new AWSAppSyncClient({
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
type: config.aws_appsync_authenticationType,
apiKey: config.aws_appsync_apiKey,
// jwtToken: async () => token, // Required when you use Cognito UserPools OR OpenID Connect. token object is obtained previously
}
})
export default class App extends React.Component {
render() {
return <ApolloProvider client={client}>
<Rehydrated>
<View style={styles.container}>
<AppNavigator />
</View>
</Rehydrated>
</ApolloProvider>
}
Here is how the subscription in a component looks like:
<Subscription subscription={gql(onCreateBlog)}>
{({data, loading})=>{
return <Text>New Item: {JSON.stringify(data)}</Text>
}}
</Subscription>
Just to add a note about the authentication as it took me a while to work this out:
If the authenticationType is "API_KEY" then you have to pass the apiKey as shown in #C.Lee's answer.
auth: {
type: config.aws_appsync_authenticationType,
apiKey: config.aws_appsync_apiKey,
}
If the authenticationType is "AMAZON_COGNITO_USER_POOLS" then you need the jwkToken, and
if you're using Amplify you can do this as
auth: {
type: config.aws_appsync_authenticationType,
jwtToken: async () => {
const session = await Auth.currentSession();
return session.getIdToken().getJwtToken();
}
}
But if your authenticationType is "AWS_IAM" then you need the following:
auth: {
type: AUTH_TYPE.AWS_IAM,
credentials: () => Auth.currentCredentials()
}
If I'm using redux and the apollo client in my app, what's the best way to trigger a query from an action outside of a component.
For example, if I have a standard app, with redux and apollo client configured, how should I trigger a "refresh" list. I can trigger a function on the component itself which has the gql, but how would I do it from an action which would be more in line with flux.
import React, { Component, PropTypes } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { connect } from 'react-redux';
import { refreshProfile } from './actions';
class Profile extends Component { ... }
Profile.propTypes = {
data: PropTypes.shape({
loading: PropTypes.bool.isRequired,
user: PropTypes.object,
}).isRequired,
};
const UserQuery = gql`
query getUser {
user {
id
name
}
}
`;
const ProfileWithData = graphql(UserQuery)(Profile);
const ProfileWithDataAndState = connect(
(state) => ({ user: state.user })),
)(ProfileWithData);
And, say I want to trigger an action to refresh that user data? Since the logic is in the component itself, I'm not sure how I would trigger that gql query from the action itself.
I would need to use the ApolloClient in my actions.js. e.g.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
const networkInterface = createNetworkInterface({
uri: config.graphCoolUri,
});
const client = new ApolloClient({
networkInterface,
dataIdFromObject: r => r.id,
});
const { data } = await client.query({
query: UserQuery
});
I see your needs, as I was just in your place couple of days ago.
The sad news is: if you want to use actions with graphQL, then you shouldn't be using apollo, just use graphQL directly. This is a very good article to walk you through - getting started with Redux and GraphQL. Why? Because Apollo uses a function called qraphql(query) which calls its own action.
How both Redux and Apollo work in a very simplistic way.
Redux: (User dispatches an action) ActionCreator --> Action --> Middleware --> reducer --> store --> bind data to user props. And we control each state manually.
Apollo: (User passes the query/mutation to graphql(query)) all hidden (action --> store) then binds data to user props.
You can say that Apollo replaces Redux if you are using graphql, because it has a better integration with react and graphQL.
In the meantime, as Apollo is still developing, you might need redux for redux-form and so on. If you are used to some redux libraries, which you might consider to continue using redux besides Apollo, you can still bind their stores and add costumed middleware that probably apply to both, but you probably won't be fetching data using Redux actions through Apollo.
I know it feels like you are loosing redux, but you are getting all advantages with more async requests and caching taking care of with Apollo.
and if you need a place to start react-redux-apollo.
I have a synchronous operation that is run somewhere down a chain of RxJS observables subscription.
This synchronous operation sets data on local storage (synchronous) that is required further down the chain in order to perform a http call (asynchronous/observable).
Here is a summary of the sequence:
Async operation returning an observable called
Sync operation setting data on local storage
Async operation using local storage date and returning an observable
Final subscription
By the time 3. is called, it seems data is not available on local storage - supposed to have been set by 2.
The above is just a simplification of the issue.
Here is the full code (in typescript):
This is called by a form (located in a component):
resetPassword() {
this.submitted = true;
if (this.passwordResetForm.valid) {
this.route.params.map(params => params['userAccountToken'])
.switchMap(userAccountToken => {
return Observable.concat(
this.userAccountService.resetPassword(Object.assign(this.passwordResetForm.value.passwordReset, {token: userAccountToken})),
this.sessionService.signinByUserAccountToken(userAccountToken)
);
})
//Will require the UserAccountResolve below which will itself fail because 'x-auth-token' is not yet available on local storage
.subscribe(() => this.router.navigate(['/dashboard']));
}
}
from UserAccountService:
resetPassword(passwordResetForm) {
return this.http.put(this.urls.USER_ACCOUNT.RESET_PASSWORD, passwordResetForm);
}
from SessionService:
signinByUserAccountToken(userAccountToken: string) {
return this.http.post(format(this.urls.AUTHENTICATION.SIGNIN_BY_USER_ACCOUNT_TOKEN, {userAccountToken}), null)
.do(response => this.setPersonalInfo(response.headers.get('x-auth-token')));
}
private setPersonalInfo(sessionToken) {
localStorage.setItem('authenticated', 'true');
localStorage.setItem('sessionToken', sessionToken);
this.authenticated$.next(true);
}
UserAccountResolve:
import {Injectable} from '#angular/core';
import {Resolve, ActivatedRouteSnapshot} from '#angular/router';
import {UserAccount} from '../shared/models/useraccount.model';
import {AuthenticatedHttpClient} from '../shared/services/authenticated-http-client.service';
import {URLS} from '../urls/URLS';
#Injectable()
export class UserAccountResolve implements Resolve<UserAccount> {
private urls;
constructor(private authenticatedHttpClient: AuthenticatedHttpClient) {
this.urls = URLS;
}
resolve(route: ActivatedRouteSnapshot) {
//Will fail
return this.authenticatedHttpClient.get(this.urls.USER_ACCOUNT.USER_ACCOUNT)
.map(response => response.json());
}
}
AuthenticatedHttpClient:
#Injectable()
export class AuthenticatedHttpClient {
static createAuthorizationHeader(headers: Headers) {
//Is not available on local storage when required
headers.append('x-auth-token', localStorage.getItem('sessionToken'));
}
constructor(private http: Http) {
}
get(url) {
let headers = new Headers();
AuthenticatedHttpClient.createAuthorizationHeader(headers);
return this.http.get(url, {
headers: headers
});
}
...
Can someone please help?
If I understand your Problem correctly, you want to delay redirecting your user until both HTTP requests have been done. Currently you subscribe to your Observable, causing the redirect to happen as soon as it observes the first result, which is in this case the result of userAccountService.resetPassword().
If you just want to wait for all requests to finish, you can subscribe onCompleted like this:
observable.subscribe(
_ => {},
err => { /* Don't forget to handle errors */ },
() => this.router.navigate(['/dashboard']))
However having an Observable whose results you do not care about is a sign, that your App might need a refactor one of these days.