Daniel Shriki
Daniel Shriki

Reputation: 41

Creating a logging middleware for jsforce library

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]); }

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.

Upvotes: 0

Views: 147

Answers (1)

Daniel Shriki
Daniel Shriki

Reputation: 41

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;

Upvotes: 0

Related Questions