0
\$\begingroup\$

Context and Existing Code

I am trying to "promisify" a third-party authentication library (auth0-js) written in JavaScript. Right now it uses callback functions and there are no plans to expose an async/await friendly API. So, I'm writing a wrapper, to avoid callback hell in my own code.

Here's a callback factory function cb which is actively used for creating Promises.

import * as a0 from "auth0-js";

export function cb<TResult>(
  resolve: (reason: TResult) => void,
  reject: (reason: a0.Auth0Error) => void,
): a0.Auth0Callback<TResult> {
  return (error, result) => error ? reject(error) : resolve(result);
}

And here's an example of a wrapper class.

import * as a0 from "auth0-js";
import { cb } from "./cb";

export class Authentication {

  /**
   * Wraps the Auth0-js' `Authentication` object and exposes Promise-based methods.
   * @param wa Wrapped `Authentication` object.
   */
  constructor(private wa: a0.Authentication) { }

  public get dbConnection(): DbConnection {
    return new DBConnection(this.wa.dbConnection);
  }

  public buildAuthorizeUrl(options: any): string {
    return this.wa.buildAuthorizeUrl(options);
  }

  public loginWithDefaultDirectory(options: a0.DefaultDirectoryLoginOptions): Promise<any> {
    return new Promise((resolve, reject) => this.wa.loginWithDefaultDirectory(options, cb(resolve, reject)));
  }

  public login(options: a0.DefaultLoginOptions): Promise<any> {
    return new Promise((resolve, reject) => this.wa.login(options, cb(resolve, reject)));
  }

  public oauthToken(options: any): Promise<any> {
    return new Promise((resolve, reject) => this.wa.oauthToken(options, cb(resolve, reject)));
  }

  public loginWithResourceOwner(options: a0.ResourceOwnerLoginOptions): Promise<any> {
    return new Promise((resolve, reject) => this.wa.loginWithResourceOwner(options, cb(resolve, reject)));
  }

  public getSSOData(withActiveDirectories: boolean, options: a0.DelegationOptions): Promise<any> {
    return new Promise((resolve, reject) => this.wa.getSSOData(withActiveDirectories, cb(resolve, reject)));
  }

  public userInfo(accessToken: string): Promise<a0.Auth0UserProfile> {
    return new Promise((resolve, reject) => this.wa.userInfo(accessToken, cb(resolve, reject)));
  }

  public delegation(options: a0.DelegationOptions): Promise<a0.Auth0DelegationToken> {
    return new Promise((resolve, reject) => this.wa.delegation(options, cb(resolve, reject)));
  }

  public getUserCountry(): Promise<{ countryCode: string; }> {
    return new Promise((resolve, reject) => this.wa.getUserCountry(cb<{ countryCode: string; }>(resolve, reject)));
  }
}

Question

While I'm always open to any constructive feedback, there are a few specific aspects I'll be particularly thankful for.

  1. I'm a C# developer and still know too little about JavaScript/TypeScript. If you know how to make this code look more JS-idiomatic, I'm all ears. Or I can paraphrase the question as "am I going the right/common route?"
  2. You can see that there is a lot of structural repetition in the code that creates Promises from callbacks (e.g. loginWithDefaultDirectory(...), login(...), oauthToken(...) functions). Not nice. Is there a way to apply functional programming ideas here? I have a feeling that functional composition may get handy here, but I don't know how to apply it (a sign, I haven't grasp the concept yet)
  3. Resolved It's also easy to notice that buildAuthorizeUrl(...) is a pass-through. No callbacks here. There must be an easier way to bind one object's function to another object's function, which I don't know how to do. Very similarly, get dbConnection() accessor is a supposed to be a pass-through.

Update 1

I figured the pass-through members can be coded very easily (one line per member):

 public buildAuthorizeUrl = this.wa.buildAuthorizeUrl;

So, point 3 is now out of question.

\$\endgroup\$
  • \$\begingroup\$ Should getSSOData use the options parameter? \$\endgroup\$ – Gerrit0 Oct 5 '17 at 0:17
1
\$\begingroup\$

You are correct that you can avoid a lot of the structure duplication in this class. A promisify function would help a lot. Node has a built in util.promisify. Alternatively, you could write a simple method to apply the arguments and resolve when called. In vanilla JS this is simple enough.

function callPromised(method, ...args) {
    return new Promise((resolve, reject) => {
        method(...args, (error, result) => error ? reject(error) : resolve(result))
    })
}

Unfortunately, this method is difficult to type correctly in TypeScript. There's a proposal here. The best we can do now is write several overloads which gets incredibly messy with only a few parameters.

function callPromised<TResult>(method: (cb: Auth0Callback<TResult>) => void): Promise<TResult>;
function callPromised<TResult, Arg1>(method: (arg1: Arg1, cb: Auth0Callback<TResult>) => void, arg1: Arg1): Promise<TResult>;
function callPromised<TResult, Arg1, Arg2>(method: (arg1: Arg1, arg2: Arg2, cb: Auth0Callback<TResult>) => void, arg1: Arg1, arg2: Arg2): Promise<TResult>;
function callPromised(method: Function, ...args: any[]): Promise<any> {
    return new Promise((resolve, reject) => {
        method(...args, (error, result) => error ? reject(error) : resolve(result))
    })
}

The downside of this function is that this will not be bound correctly. This could be rectified by passing in a self parameter and would be required for your usage.

function callPromised<TResult>(method: (cb: Auth0Callback<TResult>) => void, self: any): Promise<TResult>;
function callPromised<TResult, Arg1>(method: (arg1: Arg1, cb: Auth0Callback<TResult>) => void, self: any, arg1: Arg1): Promise<TResult>;
function callPromised<TResult, Arg1, Arg2>(method: (arg1: Arg1, arg2: Arg2, cb: Auth0Callback<TResult>) => void, self: any, arg1: Arg1, arg2: Arg2): Promise<TResult>;
function callPromised(method: Function, self: any, ...args: any[]): Promise<any> {
    return new Promise((resolve, reject) => {
        method.call(self, ...args, (error, result) => error ? reject(error) : resolve(result))
    })
}

// For example
public loginWithDefaultDirectory(options: a0.DefaultDirectoryLoginOptions): Promise<any> {
    return callPromised(this.wa.loginWithDefaultDirectory, this.wa, options)
}
\$\endgroup\$
  • \$\begingroup\$ Thank you Gerrit0! This is a very good answer which points to the right direction. In fact, I asked another question on SO about how to achieve what you're describing. I looked at implementations in both promisify-js and typed-promisify libraries, which are similar but not the same. Unfortunately, at this point TypeScript is still missing a lot of things related to smarter type inference. This leads to -- as you very correctly noted -- ugly, messy, and quickly growing overloads which have to be expressed upfront. Thanks again for the cool answer (I wish you saw my question a bit earlier) \$\endgroup\$ – Igor Soloydenko Oct 5 '17 at 6:31
  • \$\begingroup\$ And here's the link to SO question: stackoverflow.com/questions/46552920/… Plus the link to the related TypeScript feature github.com/Microsoft/TypeScript/issues/5453 Plus the link to the roadmap github.com/Microsoft/TypeScript/wiki/Roadmap \$\endgroup\$ – Igor Soloydenko Oct 5 '17 at 6:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.