Repository: auth0/node-auth0 (ts)

Issue:
Retreive response headers 
<!--
**Please do not report security vulnerabilities here**. The Responsible Disclosure Program (https://auth0.com/whitehat) details the procedure for disclosing security issues.

Thank you in advance for helping us to improve this library! Your attention to detail here is greatly appreciated and will help us respond as quickly as possible. For general support or usage questions, use the Auth0 Community (https://community.auth0.com/) or Auth0 Support (https://support.auth0.com/). Finally, to avoid duplicates, please search existing Issues before submitting one here.

By submitting an Issue to this repository, you agree to the terms within the Auth0 Code of Conduct (https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
-->

### Describe the problem you'd like to have solved

Using the management API to retrieve users which works fine. Now I need to have access to the response headers to tackle a rate limiting issue. Like described [here](https://auth0.com/docs/troubleshoot/customer-support/operational-policies/rate-limit-policy#handle-rates-limitations-in-code). I need to have access to the auth0 response headers x-ratelimit-limit,  x-ratelimit-remaining and x-ratelimit-reset. I was not able to get the response headers from auth0.

While checking the management api code I found out that the headers from the rest-fasade (used client) are not being passed back to the results.


<!--
> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-->

### The ideal solution

I would like to be able to get the headers together with the response body.
```
const auth0ApiClient = new ManagementClient(...)
auth0ApiClient.getUser({ id: customerId }.then((body, headers) => {
    console.log("Headers: ", headers)
})
```

Let me know If there is a workaround within the management API to solve this issue.


Pull request:
Add option to include response headers in the result
### Changes

Adding an option `includeResponseHeaders` to the Management Client so that you can get the response headers as well as the payload in the result of each method. eg

```js
const mgmt = new ManagementClient();
const mgmt2 = new ManagementClient({ includeResponseHeaders: true });

const users = await mgmt.getUsers();
const { data: users, headers } = await mgmt2.getUsers();

mgmt.getUsers((err, users) => {
  console.log(users);
});
mgmt.getUsers((err, { data: users, headers }) => {
  console.log(users);
});
```

This is useful for users that want to use a different rate limit retry strategy than the exponential backoff on 429 used by this SDK (Like the one described here https://auth0.com/docs/troubleshoot/customer-support/operational-policies/rate-limit-policy#handle-rates-limitations-in-code)

### References

fixes #742 #741

### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. 

- [x] This change adds unit test coverage

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All existing and new tests complete without errors


Patch:
diff --git a/src/Auth0RestClient.js b/src/Auth0RestClient.js
index 79aacded3..92080714b 100644
--- a/src/Auth0RestClient.js
+++ b/src/Auth0RestClient.js
@@ -27,7 +27,7 @@ class Auth0RestClient {
 
     this.wrappedProvider = function (method, args) {
       if (!this.provider) {
-        return this.restClient[method](...args);
+        return this._request(method, args);
       }
 
       let callback;
@@ -39,7 +39,7 @@ class Auth0RestClient {
         .getAccessToken()
         .then((access_token) => {
           this.restClient.options.headers['Authorization'] = `Bearer ${access_token}`;
-          return this.restClient[method](...args);
+          return this._request(method, args);
         })
         .catch((err) => {
           if (callback) {
@@ -50,6 +50,36 @@ class Auth0RestClient {
     };
   }
 
+  _request(method, args) {
+    if (!this.options.includeResponseHeaders) {
+      return this.restClient[method](...args);
+    }
+
+    let callback;
+    // Handle the case where the promise variant is called without any args
+    // client.get() -> client.get({}, callback)
+    if (!args || !args.length) {
+      args = [{}];
+    }
+    // Handle the case where an undefined placeholder is defined for callback
+    // client.get(params, undefined) -> client.get(params, callback)
+    if (typeof args[args.length - 1] === 'undefined') {
+      args.pop();
+    }
+    if (typeof args[args.length - 1] === 'function') {
+      callback = args.pop();
+    }
+    return new Promise((resolve, reject) => {
+      this.restClient[method](...args, (err, data, headers) => {
+        const payload = { data, headers };
+        if (err) {
+          (callback && callback(err)) || reject(err);
+        }
+        (callback && callback(null, payload)) || resolve(payload);
+      });
+    });
+  }
+
   getAll(...args) {
     return this.wrappedProvider('getAll', args);
   }
diff --git a/src/management/BaseManager.js b/src/management/BaseManager.js
index 93ec257f9..94bb9dd93 100644
--- a/src/management/BaseManager.js
+++ b/src/management/BaseManager.js
@@ -25,6 +25,7 @@ class BaseManager {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
       query: { repeatParams: false },
+      includeResponseHeaders: options.includeResponseHeaders,
     };
 
     const usersAuth0RestClient = new Auth0RestClient(
diff --git a/src/management/index.js b/src/management/index.js
index 99b1365b9..36cfd7865 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -90,6 +90,7 @@ class ManagementClient {
    * @param   {boolean} [options.retry.enabled=true]                Enabled or Disable Retry Policy functionality.
    * @param   {number}  [options.retry.maxRetries=10]               Retry failed requests X times.
    * @param   {object}  [options.headers]                           Additional headers that will be added to the outgoing requests.
+   * @param   {object}  [options.includeResponseHeaders]            Include the response headers in the payload in the format `{ data, headers }`.
    */
   constructor(options) {
     if (!options || typeof options !== 'object') {
@@ -147,6 +148,7 @@ class ManagementClient {
     }
 
     managerOptions.retry = options.retry;
+    managerOptions.includeResponseHeaders = options.includeResponseHeaders;
 
     /**
      * Simple abstraction for performing CRUD operations on the


Test patch:
diff --git a/test/auth0-rest-client.tests.js b/test/auth0-rest-client.tests.js
index a44df6e7c..707bd2457 100644
--- a/test/auth0-rest-client.tests.js
+++ b/test/auth0-rest-client.tests.js
@@ -228,4 +228,36 @@ describe('Auth0RestClient', () => {
       done();
     });
   });
+
+  it('should include response headers in promise response', async function () {
+    nock(API_URL).get('/some-resource').reply(200, { data: 'value' });
+
+    const options = {
+      includeResponseHeaders: true,
+      headers: {},
+    };
+
+    const client = new Auth0RestClient(`${API_URL}/some-resource`, options, this.providerMock);
+    const { data, headers } = await client.getAll();
+    expect(data).to.deep.equal({ data: 'value' });
+    expect(headers).to.deep.equal({ 'content-type': 'application/json' });
+    nock.cleanAll();
+  });
+
+  it('should include response headers in callback response', function (done) {
+    nock(API_URL).get('/some-resource').reply(200, { data: 'value' });
+
+    const options = {
+      includeResponseHeaders: true,
+      headers: {},
+    };
+
+    const client = new Auth0RestClient(`${API_URL}/some-resource`, options, this.providerMock);
+    client.getAll((err, { data, headers }) => {
+      expect(data).to.deep.equal({ data: 'value' });
+      expect(headers).to.deep.equal({ 'content-type': 'application/json' });
+      nock.cleanAll();
+      done();
+    });
+  });
 });
diff --git a/test/management/management-client.tests.js b/test/management/management-client.tests.js
index 78c4d89ae..8b38001af 100644
--- a/test/management/management-client.tests.js
+++ b/test/management/management-client.tests.js
@@ -940,5 +940,29 @@ describe('ManagementClient', () => {
       expect(this.client.users.assignRoles.called).ok;
       this.client.users.assignRoles.reset();
     });
+
+    it('should include response headers in response', async function () {
+      const config = Object.assign({}, withTokenConfig, { includeResponseHeaders: true });
+      this.client = new ManagementClient(config);
+
+      nock('https://auth0-node-sdk.auth0.com').get(`/api/v2/users`).reply(200, { data: 'value' });
+
+      const { data, headers } = await this.client.users.getAll();
+      expect(data).to.deep.equal({ data: 'value' });
+      expect(headers).to.deep.equal({ 'content-type': 'application/json' });
+      nock.cleanAll();
+    });
+
+    it('should include response headers in response for shorthand method', async function () {
+      const config = Object.assign({}, withTokenConfig, { includeResponseHeaders: true });
+      this.client = new ManagementClient(config);
+
+      nock('https://auth0-node-sdk.auth0.com').get(`/api/v2/users`).reply(200, { data: 'value' });
+
+      const { data, headers } = await this.client.getUsers();
+      expect(data).to.deep.equal({ data: 'value' });
+      expect(headers).to.deep.equal({ 'content-type': 'application/json' });
+      nock.cleanAll();
+    });
   });
 });