如何使用拦截器访问 nest.js 中的最终 GraphQL-Reponse

rwe*_*sse 9 node.js graphql nestjs

我已经实现了一个LoggingInterceptor应该能够访问最终 GraphQL-Response 及其数据和错误属性 + 原始请求正文和经过身份验证的用户,该用户AuthGuard之前已添加到请求中。(编辑:@jay-mcdoniel 部分解决:user并且body可以通过 访问GqlExecutionContext.create(context).getContext()

事实上,拦截器只是提供了一个完全解析的 GraphQL 对象。

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(tap(
      (allData) => console.log(allData),
      (error)=> console.log(error)));
  }
}
Run Code Online (Sandbox Code Playgroud)

这是我的拦截器级。它只是调用 RxJS-Operatortap来记录 observable 的当前值。

如果我运行以下 GraphQL-Request ...

mutation {
  login(data: { username: "admin", password: "123456" }) {
    id
    username
    token
  }
}
Run Code Online (Sandbox Code Playgroud)

...我的服务器使用以下响应正文正确回答:

{
  "data": {
    "login": {
      "id": "6f40be3b-cda9-4e6d-97ce-ced3787e9974",
      "username": "admin",
      "token": "someToken"
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

但是allData我的拦截器记录到控制台的内容如下:

{
  id: '6f40be3b-cda9-4e6d-97ce-ced3787e9974',
  isAdmin: true,
  username: 'admin',
  firstname: null,
  lastname: null,
  email: null,
  created: 2019-07-05T15:11:31.606Z,
  token: 'someToken'
}
Run Code Online (Sandbox Code Playgroud)

相反,我想查看真实响应主体的信息。

我还尝试通过context.switchToHttp().getResponse(). 但这仅包含mutation-login-method的参数:

{
  data: [Object: null prototype] { username: 'admin', password: '123456' }
}
Run Code Online (Sandbox Code Playgroud)

编辑

console.log(GqlExecutionContext.create(context).getContext()); 打印(仍然没有 GraphQL-ResponseBody):

{
  headers: {
    /*...*/
  },
  user: /*...*/,
  body: {
    operationName: null,
    variables: {},
    query: 'mutation {\n  login(data: {username: "admin", password: ' +
      '"123456"}) {\n    token\n    id\n    username\n    isAdmin\n  }\n' +
      '}\n'
  },
  res: ServerResponse {
    _events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
    _eventsCount: 1,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      readable: true,
      _events: [Object],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: true,
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      timeout: 120000,
      parser: [HTTPParser],
      on: [Function: socketOnWrap],
      _paused: false,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 566,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        /*...*/
      },
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    connection: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      readable: true,
      _events: [Object],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: true,
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      timeout: 120000,
      parser: [HTTPParser],
      on: [Function: socketOnWrap],
      _paused: false,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 566,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: 120000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 3273,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(asyncId)]: 567,
        [Symbol(triggerId)]: 566
      },
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _header: null,
    _onPendingData: [Function: bound updateOutgoingData],
    _sent100: false,
    _expect_continue: false,
    req: IncomingMessage {
      /*...*/
    },
    locals: [Object: null prototype] {},
    [Symbol(isCorked)]: false,
    [Symbol(outHeadersKey)]: [Object: null prototype] {
      'x-powered-by': [Array],
      'access-control-allow-origin': [Array]
    }
  },
  _extensionStack: GraphQLExtensionStack { extensions: [ [CacheControlExtension] ] }
}
Run Code Online (Sandbox Code Playgroud)

小智 8

这就是我在日志拦截器中所做的

    @Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // default REST Api
    if (context.getType() === 'http') {
     ...
     ...
    }

    // Graphql
    if (context.getType<GqlContextType>() === 'graphql') {
      const gqlContext = GqlExecutionContext.create(context);
      const info = gqlContext.getInfo();
      const res: Response = gqlContext.getContext().res;
      // Get user that sent request
      const userId = context.getArgByIndex(2).req.user.userId;
      const parentType = info.parentType.name;
      const fieldName = info.fieldName;
      const body = info.fieldNodes[0]?.loc?.source?.body;
      const message = `GraphQL - ${parentType} - ${fieldName}`;

      // Add request ID,so it can be tracked with response
      const requestId = uuidv4();
      // Put to header, so can attach it to response as well
      res.set('requestId', requestId);

      const trace = {
        userId,
        body
      };

      this.logger.info(`requestId: ${requestId}`, {
        context: message,
        trace
      });
      
      return next.handle().pipe(
        tap({
          next: (val: unknown): void => {
            this.logNext(val, context);
          }
        })
      );
    }
    return next.handle();
  }

  /**
   * Method to log response message
   */
  private logNext(body: unknown, context: ExecutionContext): void {
    // default REST Api
    if (context.getType() === 'http') {
      ...
      ...
    }

    if (context.getType<GqlContextType>() === 'graphql') {
      const gqlContext = GqlExecutionContext.create(context);
      const info = gqlContext.getInfo();
      const parentType = info.parentType.name;
      const fieldName = info.fieldName;
      const res: Response = gqlContext.getContext().res;
      const message = `GraphQL - ${parentType} - ${fieldName}`;

      // Remove secure fields from request body and headers
      const secureBody = secureReqBody(body);

      const requestId = res.getHeader('requestId');

      // Log trace message
      const trace = {
        body: { ...secureBody }
      };
      this.logger.info(`requestId: ${requestId}`, {
        context: message,
        trace
      });
    }
  }
}
Run Code Online (Sandbox Code Playgroud)