How to inject a promise into a promise chain with an executor?

Viewed 70

What I'm trying to accomplish

I am currently trying to create a wrapper for a db connection (to Neo4j) that works similar to the following:

  1. Instantiate driver
  2. Expose the executor for the driver so a session can be created
  3. Pass my logic
  4. Close the connection

Since there's no destructor in JavasScript, it's making this unnecessarily difficult with properly closing the session. The logic for creating and closing a connection is extremely repetitive and I'm trying to simplify repetitive scripts so that it's easier to call.


What I've tried.

Inject promise in chain

I thought something like the following could work, but I just cannot seem to create the logic properly. Passing session back to my inserted promise is challenging.

const connect = () => {
    var driver;
    var session;

    return Promise.resolve(() => {
        driver = my.driver(uri, creds);
    }).then(() => {
        // insert my promise here, exposing driver.session() function for executor
        // if possible, bind it to the session var so we can properly close it after
        // this would also determine the return value
    }).catch((e) => console.error(e))
    .finally(() => {
        session.close();
        driver.close();
    })
});

Create class wrapper with procedural logic

I then also tried another approach, similar to:

var driver = my.driver(uri, creds);
var session;

function exitHandler(options) {
    // ...
    session.close();
    driver.close();
}

// I have process.on for exit, SIGINT, SIGUSR1, SIGUSR2, and uncaughtException
process.on('exit', exitHandler.bind(null, options));
// ...

class Connection extends Promise<any> {
    constructor(executor: Function) {
        super((resolve, reject) => executor(resolve, reject));
        executor(driver.session.bind(null, this));
    }
}

export default Connection;

And calling it like

// ...

const handler = async () => await new Connection((session) => {
    const s = session();
    // do stuff here
});

The problem with this approach is that the driver is not instantiated before session is used (and so it's undefined). It also feels a little hacky with the process.on calls.


Question

Neither method works (or any of my other attempts). How can I properly wrap db connections to ensure they're consistent and deduplicate my existing code?

A sample of the Neo4j connection script can be found here. This is, essentially, what I'm trying to deduplicate across my scripts (pass everything from line 11 to 42 - inclusive) but have the init of driver, catch, finally, session.close(), driver.close() logic in my wrapper.

Ideally, I would like to expose the session function call so that I can pass parameters to it if needed: See the Session API for more info. If possible, I also want to bind the rxSession reactive session.

1 Answers

A sample of the Neo4j connection script can be found here. This is, essentially, what I'm trying to deduplicate across my scripts (pass everything from line 11 to 42 - inclusive) but have the init of driver, catch, finally, session.close(), driver.close() logic in my wrapper.

OK, the above part of what you are asking is what I was able to best parse and work with.

Taking the code you reference and factoring out lines 11 to 42 such that everything outside of those is shared and everything inside of those is customizable by the caller, this is what I get for the reusable part, designed to be in a module by itself:

// dbwrapper.js

const neo4j = require('neo4j-driver')

const uri = 'neo4j+s://<Bolt url for Neo4j Aura database>';
const user = '<Username for Neo4j Aura database>';
const password = '<Password for Neo4j Aura database>';

const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
let driverOpen = true;

async function runDBOperation(opCallback, sessOpts = {}) {
    const session = driver.session(sessOpts);
    try {
        await opCallback(session);
    } catch (e) {
        console.log(e);
        throw e;
    } finally {
        await session.close();
    }
}

async function shutdownDb() {
    if (driverOpen) {
        driverOpen = false;
        await driver.close();
    }
}

process.on('exit', shutdownDb);

module.exports = { runDBOperation, shutdownDb };

Then, you could use this from some other module like this:

const { runDBOperation, shutdownDB } = require('./dbwrapper.js');

runDBOperation(async (session) => {
    const person1Name = 'Alice'
    const person2Name = 'David'

    // To learn more about the Cypher syntax, see https://neo4j.com/docs/cypher-manual/current/
    // The Reference Card is also a good resource for keywords https://neo4j.com/docs/cypher-refcard/current/
    const writeQuery = `MERGE (p1:Person { name: $person1Name })
                      MERGE (p2:Person { name: $person2Name })
                      MERGE (p1)-[:KNOWS]->(p2)
                      RETURN p1, p2`

    // Write transactions allow the driver to handle retries and transient errors
    const writeResult = await session.writeTransaction(tx =>
        tx.run(writeQuery, { person1Name, person2Name })
    )
    writeResult.records.forEach(record => {
        const person1Node = record.get('p1')
        const person2Node = record.get('p2')
        console.log(
            `Created friendship between: ${person1Node.properties.name}, ${person2Node.properties.name}`
        )
    })

    const readQuery = `MATCH (p:Person)
                     WHERE p.name = $personName
                     RETURN p.name AS name`
    const readResult = await session.readTransaction(tx =>
        tx.run(readQuery, { personName: person1Name })
    )
    readResult.records.forEach(record => {
        console.log(`Found person: ${record.get('name')}`)
    })
}).then(result => {
    console.log("all done");
}).catch(err => {
    console.log(err);
});

This can be made more flexible or more extensible according to requirements, but obviously the general idea is to keep it simple so that simple uses of the common code don't require a lot of code.

Related