1

I am trying to import and delete large numbers of users from Okta while staying within rate limits, and logging any errors to excel. The code below seems to be working, but there is just the issue that the last of the 5 errors I see on the console log does not appear in the outputted CSV.

I have tried a range of alternatives including putting the csvWriter call in a .then rather than the .finally. The issue is that it is not waiting for the last error to be pushed to the array.

"use strict";
const okta = require("@okta/okta-sdk-nodejs");
const csv = require("csv-parser");
const fs = require("fs");
const createCsvWriter = require("csv-writer").createObjectCsvWriter;

let timeRun = new Date()
  .toISOString()
  .replace(/T/, " ") // replace T with a space
  .replace(/\..+/, "") // delete the dot and everything after
  .replace(/:/g, "."); // replace T with a space

const csvWriter = createCsvWriter({
  path: "errorLog-" + timeRun + ".csv",
  header: [
    { id: "error", title: "Error" },
    { id: "row", title: "Row" },
  ],
});
let record = [];
let currentError = {}

// Enter tenant info and API key here
const client = new okta.Client({
  orgUrl: "https://xxxxxxxx.oktapreview.com",
  token: "xxxxxxxxxxxxxxxxxxxxxxx",
});

let usersToDelete = [];
let currentUserID;

var getUsersToDelete = new Promise((resolve, reject) => {
  fs.createReadStream("testImport.csv")
    .pipe(csv())
    .on("data", (row) => {
      usersToDelete.push(row);
    })
    .on("end", (row) => {
      resolve();
    });
});

getUsersToDelete
  .then(async () => {
    let iCount = 1;
    while (usersToDelete.length > 0) {
        
      var deleteUserTimeout = new Promise((resolve, reject) => {
        setTimeout(async function () {
          currentUserID = usersToDelete.pop();
          client
            .getUser(currentUserID.email)
            .then(async (user) => {
              return user
                .deactivate()
                .then(() => console.log("User has been deactivated"))
                .then(() => user.delete())
                .then(() => console.log("User has been deleted"));
            })
            .catch(async function (error, row) {
                    currentError = { error: error.message, row: "row" };
                    console.error(currentError);
              return error;
            })
            .finally(() => {
              record.push(currentError);
              reject()
            });
          resolve();
        }, 2000);
      });
      await deleteUserTimeout;
      console.log("Timeout" + currentUserID, "Iteration: " + iCount);
      iCount++
    }
  }).finally(async () => {
    await csvWriter.writeRecords(record);
  });
Jake Durell
  • 169
  • 1
  • 12

2 Answers2

0

There's a race in your deleteUserTimeout promise. It resolves immediately after client.getUser without awaiting the result.

var deleteUserTimeout = new Promise((resolve, reject) => {
  setTimeout(async function () {
    currentUserID = usersToDelete.pop();
    client.getUser(currentUserID.email) // forgot to await this
    //... snip
    resolve(); // resolves immediately
  }, 2000);
});
await deleteUserTimeout; // this won't wait for the delete

I'd refactor it for clarity (not tested):

async function deleteUser(email) {
  let user = await client.getUser(email);
  await user.deactivate();
  await user.delete();
}

function sleep(timeoutMs) {
  return new Promise(_ => setTimeout(_, timeoutMs));
}

async function deleteUsers(usersToDelete) {
  const errors = [];

  for (let row = 0; row < usersToDelete.length; row++) {
    let currentUserID = usersToDelete[row];
    await sleep(2000);
    await deleteUser(currentUserID.email)
      .catch(error => {
        errors.push({error: error.message, row});
      });
  }

  return errors;
}
teppic
  • 7,051
  • 1
  • 29
  • 35
0

Your deleteUserTimeout promise is not waiting for the completion of the user delete operation, and the script finishes almost immediately after it starts setTimeout callback for the last user to be deleted. CSV would be written with all the errors accumulated in record by that timing, not waiting for some operations to complete including the 5 iterations that encounter errors.

Basic idea is the same with @teppic's, but you may want to write out each error as you find, instead of delaying them till the end, to reduce the possibility of losing the error info in case of your script crashes with whatever reasons (e.g. machine power down).

const sleep = time => new Promise(resolve => setTimeout(resolve, time))

const deleteUser = async email => {
  const user = await client.getUser(email)
  await user.deactivate()
  await user.delete()
}

const deleteUsers = async users => {
  for (const user of users) {
    try {
      await deleteUser(user.email)
      console.log(`Deleted ${user.email}`)
      await sleep(2000)
    } catch (e) {
      console.error(e.message)
      await csvWriter.writeRecords([{error: e.message, row: 'row'}])
    }
  }
}

getUsersToDelete.then(() => deleteUsers(usersToDelete))
ryu1kn
  • 457
  • 5
  • 9