Repository: auth0/node-auth0 (ts)

Issue:
Missing user exports endpoint?
Hi,

Are there plans to implement the `/api/v2/jobs/users-exports` endpoint? I was looking at `JobsManager.js` and could only see the import users endpoint implemented.

If not, I can probably help implement it as we have a need for it.

Thanks.

Pull request:
Implement users exports endpoint
### Changes
This PR implements the `/api/v2/jobs/users-exports` endpoint.

Instead of the approach in #319 this PR uses `Auth0RestClient` and `RetryRestClient` which results in a much shorter implementation (i.e. with retry if the rate limit is hit and the management API returns a HTTP 429 response).

### References
Fixes #318 

### Testing
To test the changes in this PR:

- Checkout this branch
- Create file `test.js` in the root directory of this branch with the following contents:
    ```javascript
    const src = require("./src");
    const auth0 = new src.ManagementClient({
      "domain": "<domain>",
      "clientId": "<clientId>",
      "clientSecret": "<clientSecret>",
      "scope": "read:users"
    });
    return auth0.exportUsers({ format: "csv", })
      .then(console.log)
      .catch(console.error);
    ```
    Make sure to fill out the `domain`, `clientId` and `clientSecret`.
- Run `node test.js`
- Verify that the console output shows a JSON response of a created export job.

- [x] This change adds unit test coverage

- [ ] This change adds integration 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/management/JobsManager.js b/src/management/JobsManager.js
index 4899174db..3c244f290 100644
--- a/src/management/JobsManager.js
+++ b/src/management/JobsManager.js
@@ -57,6 +57,19 @@ var JobsManager = function(options) {
     options.tokenProvider
   );
   this.jobs = new RetryRestClient(auth0RestClient, options.retry);
+
+  /**
+   * Provides an abstraction layer for consuming the
+   * {@link https://auth0.com/docs/api/v2#!/Jobs/post_users_exports Create job to export users endpoint}
+   *
+   * @type {external:RestClient}
+   */
+  const usersExportsRestClient = new Auth0RestClient(
+    options.baseUrl + '/jobs/users-exports',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.usersExports = new RetryRestClient(usersExportsRestClient, options.retry);
 };
 
 /**
@@ -194,6 +207,63 @@ JobsManager.prototype.importUsers = function(data, cb) {
   return promise;
 };
 
+/**
+ * Export all users to a file using a long running job.
+ *
+ * @method   exportUsers
+ * @memberOf module:management.JobsManager.prototype
+ *
+ * @example
+ * var data = {
+ *   connection_id: 'con_0000000000000001',
+ *   format: 'csv',
+ *   limit: 5,
+ *   fields: [
+ *     {
+ *       "name": "user_id"
+ *     },
+ *     {
+ *       "name": "name"
+ *     },
+ *     {
+ *       "name": "email"
+ *     },
+ *     {
+ *       "name": "identities[0].connection",
+ *       "export_as": "provider"
+ *     },
+ *     {
+ *       "name": "user_metadata.some_field"
+ *     }
+ *   ]
+ * }
+ *
+ * management.jobs.exportUsers(data, function (err, results) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Retrieved job.
+ *   console.log(results);
+ * });
+ *
+ * @param   {Object}    data                  Users export data.
+ * @param   {String}    [data.connection_id]  The connection id of the connection from which users will be exported
+ * @param   {String}    [data.format]         The format of the file. Valid values are: "json" and "csv".
+ * @param   {Number}    [data.limit]          Limit the number of records.
+ * @param   {Object[]}  [data.fields]         A list of fields to be included in the CSV. If omitted, a set of predefined fields will be exported.
+ * @param   {Function}  [cb]                  Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+JobsManager.prototype.exportUsers = function(data, cb) {
+  if (cb && cb instanceof Function) {
+    return this.usersExports.create(data, cb);
+  }
+
+  return this.usersExports.create(data);
+};
+
 /**
  * Send a verification email to a user.
  *
diff --git a/src/management/index.js b/src/management/index.js
index fbdc550ab..ee75f5964 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -1611,6 +1611,57 @@ utils.wrapPropertyMethod(ManagementClient, 'getJob', 'jobs.get');
  */
 utils.wrapPropertyMethod(ManagementClient, 'importUsers', 'jobs.importUsers');
 
+/**
+ * Export all users to a file using a long running job.
+ *
+ * @method   exportUsers
+ * @memberOf module:management.ManagementClient.prototype
+ *
+ * @example
+ * var data = {
+ *   connection_id: 'con_0000000000000001',
+ *   format: 'csv',
+ *   limit: 5,
+ *   fields: [
+ *     {
+ *       "name": "user_id"
+ *     },
+ *     {
+ *       "name": "name"
+ *     },
+ *     {
+ *       "name": "email"
+ *     },
+ *     {
+ *       "name": "identities[0].connection",
+ *       "export_as": "provider"
+ *     },
+ *     {
+ *       "name": "user_metadata.some_field"
+ *     }
+ *   ]
+ * }
+ *
+ * management.exportUsers(data, function (err, results) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Retrieved job.
+ *   console.log(results);
+ * });
+ *
+ * @param   {Object}    data                  Users export data.
+ * @param   {String}    [data.connection_id]  The connection id of the connection from which users will be exported
+ * @param   {String}    [data.format]         The format of the file. Valid values are: "json" and "csv".
+ * @param   {Number}    [data.limit]          Limit the number of records.
+ * @param   {Object[]}  [data.fields]         A list of fields to be included in the CSV. If omitted, a set of predefined fields will be exported.
+ * @param   {Function}  [cb]                  Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'exportUsers', 'jobs.exportUsers');
+
 /**
  * Send a verification email to a user.
  *


Test patch:
diff --git a/test/management/jobs.tests.js b/test/management/jobs.tests.js
index fbd7e00f9..b8b7cb98c 100644
--- a/test/management/jobs.tests.js
+++ b/test/management/jobs.tests.js
@@ -27,7 +27,7 @@ describe('JobsManager', function() {
   });
 
   describe('instance', function() {
-    var methods = ['verifyEmail', 'importUsers', 'get'];
+    var methods = ['verifyEmail', 'importUsers', 'exportUsers', 'get'];
 
     methods.forEach(function(method) {
       it('should have a ' + method + ' method', function() {
@@ -400,6 +400,82 @@ describe('JobsManager', function() {
     });
   });
 
+  describe('#exportUsers', function() {
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .post('/jobs/users-exports')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.jobs.exportUsers({ format: 'csv' }, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.jobs
+        .exportUsers({ format: 'csv' })
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should pass any errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+      nock(API_URL)
+        .post('/jobs/users-exports')
+        .reply(500);
+
+      this.jobs.exportUsers({ format: 'csv' }).catch(function(err) {
+        expect(err).to.exist;
+        done();
+      });
+    });
+
+    it('should pass the body of the response to the "then" handler', function(done) {
+      nock.cleanAll();
+
+      var data = {
+        type: 'users_export',
+        status: 'pending',
+        format: 'csv',
+        created_at: '',
+        id: 'job_0000000000000001'
+      };
+      nock(API_URL)
+        .post('/jobs/users-exports')
+        .reply(200, data);
+
+      this.jobs.exportUsers({ format: 'csv' }).then(function(response) {
+        expect(response).to.be.an.instanceOf(Object);
+        expect(response.status).to.equal('pending');
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/jobs/users-exports', function(done) {
+      var request = this.request;
+
+      this.jobs.exportUsers({ format: 'csv' }).then(function() {
+        expect(request.isDone()).to.be.true;
+        done();
+      });
+    });
+
+    it('should include the token in the Authorization header', function(done) {
+      nock.cleanAll();
+      var request = nock(API_URL)
+        .post('/jobs/users-exports')
+        .matchHeader('Authorization', 'Bearer ' + token)
+        .reply(200);
+
+      this.jobs.exportUsers({ format: 'csv' }).then(function() {
+        expect(request.isDone()).to.be.true;
+        done();
+      });
+    });
+  });
+
   describe('#verifyEmail', function() {
     var data = {
       user_id: 'github|12345'