Creating a logging middleware for jsforce library

24 Views Asked by At

I'm using jsforce library for communicating with Salesforce. This is a backend service (one of the microservices) - used by other services.

I want to log every call to Salesforce, and as far as I was searching and investigating, such an ability does not exist in the library.

After investigating, I've found one way to achieve this. I'm not sure if this is the right approach (I didn't find another one), And this approach also makes problems...

The most common way that I'm using this library is first, creating a connection with:

conn = new jsforce.Connection

and afterwards for example, conn.sobject(Account).retrieve(...), ect.

So I wrapped the connection creation with SalesforceClient object, that will wrap sobject with a new SobjectWrapper object that utilizes javascript Proxy:

class SobjectWrapper {
    constructor(conn, sob, correlationId) {
        this.conn = conn;
        this.sob = sob;
        this.correlationId = correlationId;
    }

    logAndExecute(method, methodName, args) {
        const logInfo = {
            operation: `${this.sob} - ${methodName}`,
            data: {
                ...args[0],
                correlation_id: this.correlationId
            },
        };

        logger.info('Request to SF!', logInfo);
        console.log('options: ', Object.getPrototypeOf(this.conn.sobject(this.sob).find(args[0])));
        // if (methodName == 'find') {
        //   return this.conn.sobject(this.sob).find(args[0]);
        // }
        return method
            .apply(this.conn.sobject(this.sob), args)
            .then((response) => {
                return response;
            })
            .catch((error) => {
                logger.error('Error from SF!', {
                    error
                });
                throw error; // Propagate the error after logging
            });
    }

    setCorrelationId(correlationId) {
        this.correlationId = correlationId;
    }

    // Proxy all functions to add logging
    createProxy() {
        return new Proxy(this.conn.sobject(this.sob), {
            get: (target, prop) => {
                if (typeof target[prop] === 'function') {
                    console.log('------------------'); // TODO: remove
                    return (...args) => this.logAndExecute(target[prop], prop, args);
                }
                return target[prop];
            },
        });
    }
}

class SalesforceClient {
    constructor() {
        this.conn = new jsforce.Connection({
            oauth2: {
                loginUrl: SF_LOGIN_URL,
                serverUrl: SF_SERVER_URL,
                clientId: SF_APP_ID,
                clientSecret: SF_APP_SECRET,
            },
        });
        this.conn.login(SF_USERNAME, SF_PASSWORD, function handleConnections(err) {
            if (err) {
                throw new Error(`Salesforce login faild - ${err.message}`, {});
            }
        });
        this.correlationId = null;
    }

    setCorrelationId(correlationId) {
        this.correlationId = correlationId;
    }

    sobject(sob) {
        return new SobjectWrapper(this.conn, sob, this.correlationId).createProxy();
    }
}

The idea is to call the normal library's function, but just before that to log everything I need.

The problem with this approach is logAndExecute function. When I have a call like: conn.sobject(Account).find({...}).execute(...) (chains more functions) It seems like the object looses its context, and I'm getting an error: conn.sobject(...).find(...).execute is not a function .

When I'm printing the prototype of this.conn.sobject(this.sob).find(args[0]) I can clearly see many other functions that are chained to find({...}), and I would like to still have those in the response's context.

And, Indeed, if I'm doing something like if (methodName == 'find') { return this.conn.sobject(this.sob).find(args[0]); }

  • calling the function directly instead of using apply, the context is preserved.

I don't want to call to each function separately, because there are many functions under this.conn.sobject(this.sob), and it will make me do a long if-else process. It's not so bad, but it's probably not ideal ether.

Is it possible to somehow bind this context to the result?

Do you think there is another approach to tackle this issue?

Would greatly appreciate to hear your opinions! thanks.

1

There are 1 best solutions below

0
Daniel Shriki On

Seems my approach was good, only I had tiny a thing missing in my understanding of jsforce library.

Apparently method.apply(this.conn.sobject(this.sob), args) doesn't return a promise, but an object that happens to have a method then .

So doing this inside logAndExecute solved the issue with this approach:

let result;
try {
  result = method.apply(this.conn.sobject(this.sob), args);
} catch (err) {
  logger.error('Error from SF!', { ...logInfo, error: err });
  throw err;
}
return result;