setting per-user session variable in pg-promise - pg-promise

I'm trying to set session level variable on a connection in pg-promise, the variable value will be read by trigger in the database level with current_setting('var_name'). This session variable will be different for each user, while I'm also sharing the same database connection for all users.
I found this somewhat related question utilizing the connect event, but I have concern that since I'm sharing the same connection for all users that the session var will not be set correctly when different users call the query method.
Is there another way to safely set this session var and make sure that it's isolated for each user while still sharing the same database object?
const pgPromise = pgp({
promiseLib: Promise, // overriding the default (ES6 Promise)
async connect(client, dc, useCount) {
// const cp = client.connectionParameters;
// console.log('Connected to database:', cp.database, dc);
if (dc && dc.email) {
console.log(useCount);
const email = encodeURI(dc.email);
await client.query(`SET SESSION "app.user" = '${email}'`);
}
}
});
get tenantDb() {
const state = this.request.sessionState();
const config = { host, database, port };
// pass state as database context,
// we'll get warning of duplicate database object for the same connection
const connection = pgPromise(config, state);
return connection;
}
Update
It turns out that I need to upgrade pg-promise version to the latest version, I was using v7 which doesn't differentiate the connection object based on the context, once I upgrade pg-promise to v10 the warning disappear, a more optimized solution would be if we can somehow set the session settings along with the schema callback in the initOptions parameter when we initiate the database, that way we only need one extra query execution together with the schema.

Related

Proper way to cleanup a mongo db() reference?

I'm making a multi tenant app using mongo db and would like to know what the proper procedure between switching between databases is. I know I can get a new reference to a database using the db() command:
const client = await MongoClient.connect(url);
client.mainDb = client.db('main');
app.set('mongoClient', client);
On bootup I get and store a reference to my main for all my global app data. Then each request also passes in a tenant id. I'm using Feathersjs which provides me with a hook for every request before and after.
In my before hook, I get a reference to the clients data and store it to be used during that singular request:
app.hooks({
before: {
all: [(context) => {
// Run before all API requests
const tenant = context.params?.query?.$tenant;
const tenantDbName = ... // some logic to query the tenant db name
const client = context.app.get('mongoClient');
context.params.tenantDb = client.db(tenantDbName);
}]
}
}
After the request, I'm unclear on if I should do anything to cleanup the connection. Do I just let the garbage collector clean it up since its request that was made which has ended? Or is there a function in Mongo to clean it up?
app.hooks({
after: {
all: [(context) => {
// Cleanup DB or reset connection?
context.params.tenantDb = null;
}]
}
}
I just need to ensure that the next request doesn't use a previous requests database as this could serve them other users data.

pg-promise: Update existing connection with new password

I have a use case that many connections to the database are created dynamically using pg-promise. Sometimes I need to connect again to the same database and user however the password changed.
Is there a way to update an existing connection so I dont get the "WARNING: Creating a duplicate database object for the same connection."?
Editing for better explanation:
Context
I have a non-traditional application that is a node service that handles geospatial data aquisition in the software QGIS, with Postgres + PostGIS.
This application creates temporary users in the PostgreSQL server and manage permissions on the tables and columns based on the type of work the user needs to do.
Code
const dbs = {} //global variable that stores all connections
const getConnection = async (user, password, server, port, dbname) => {
const connString = `postgres://${user}:${password}#${server}:${port}/${dbname}`
if (connString in dbs) {
return dbs[connString] //if connection already exists returns the connection
}
dbs[connString] = db.pgp(connString) //create new connection
await dbs[connString] //tests if connections is correct
.connect()
.then(obj => {
obj.done() // success, release connection;
})
.catch(e => {
errorHandler.critical(e)
})
return dbs[connString]
}
What I want is add another case, that if the connection already exists but the password changed it updates the existing connection password (or destroy it and create a new one).
The issue in your case is that you are using password as part of the connection-string key, which isn't used within the library's unique-connection check, hence the side effect.
For the key, you need to use a unique connection string that does not contain the password. And when the request is made, you need to update the connection details.
Example below makes use of the connection object, not the connection string, because it is simpler that way. But if you want, you can use a connection string too, you would just need to generate a separate connection string, with the password, and update $pool.options.connectionString, not $pool.options.password.
const dbs = {}; // global variable that stores all connections
const getConnection = async (user, password, host, port, database) => {
const key = `${user}#${host}:${port}/${database}`; // unique connection key
const cn = { host, port, database, user, password }; // actual connection
let db; // resulting database object
if (key in dbs) {
db = dbs[key];
db.$pool.options.password = password; // updating the password
} else {
db = pgp(cn); // creating new connection
dbs[key] = db;
await db // test if can connect
.connect()
.then(obj => {
obj.done(); // success, release connection;
})
.catch(e => {
errorHandler.critical(e);
throw e;
})
}
return db;
}

Module export of pg-promise object derived from promise chain

We're using HashiCorp's Vault to store database connection credentials, then constructing the connection string for pg-promise from those. The 'catch' is that the Vault details are provided from a Promise wrapper, due to request callbacks to the Vault API.
Example database.js module:
const pgp = require('pg-promise')(/* options obj */);
const getDbo = () => {
return new Promise( (resolve, reject) => {
vault.init().then(secrets => {
let credentials = secrets.dbUser + ':' + secrets.dbPass
let connStr = 'postgres://' + credentials + '<#endpoint/db>'
let dbo = pgp(connStr, (err) => {
reject(err)
})
resolve(dbo);
})
}
module.exports = { get: getDbo }
This is being imported in multiple routes. With this we are seeing the warning "WARNING: Creating a duplicate database object for the same connection." Is there a better way to resolve this so there is only one object per connection details?
Creating and initializing a connection for pg-promise is a completely synchronous operation, as per the API, so there is no point using promises for that.
For initializing the library see Where should I initialize pg-promise.
See also:
Verify database connection with pg-promise when starting an app.

Why am I getting this 'undefined' error?

I'm working on a Meteor project, and for some reason this profile template refuses to work.
I'm using the following code, as well as the accounts-password and accounts-entry packages for user management:
this.route('profile', {
path: '/profile/:username',
data: function() {
var userDoc = Meteor.users.findOne({"username": this.params.username});
var bookCursor = Books.find({owner: userDoc._id});
return {
theUser: userDoc,
theBooks: bookCursor
};
}
});
When I try to go to the profile URL for my test accounts ('misutowolf', and 'test2', respectively), I am given the following error in Chrome's dev console: Exception from Deps recompute function: TypeError: Cannot read property '_id' of undefined, pointing to the use of userDoc._id in the call to Books.find().
This makes no sense whatsoever, as I was able to find a user document with the names in question using meteor mongo with both usernames, in the form db.users.find({username: "misutowolf"}) and db.users.find({username: "test2"}).
I am very confused, not sure what is causing this issue at all.
By default Meteor only publish the currently logged in user info via an automatically setup publication.
What you need to do is push to the client the user info (username) you're trying to use, because if you don't do that, the user you're accessing is not published to the client and you get an undefined error when accessing its _id.
First, setup a dedicated publication (on the server) :
Meteor.publish("userByUsername",function(username){
return Meteor.users.find({
username:username
});
});
Then waitOn this publication in your route :
waitOn:function(){
return this.subscribe("userByUsername",this.params.username);
}
Finally, guard against accessing the user document until it is pushed to the client because even if you are waiting on the subscription, the data method might actually get called even if the subscription is not ready yet.
data: function() {
var userDoc = Meteor.users.findOne({"username": this.params.username});
if(!userDoc){
return;
}
// ...
}

Updating MongoDB in Meteor Router Filter Methods

I am currently trying to log user page views in meteor app by storing the userId, Meteor.Router.page() and timestamp when a user clicks on other pages.
//userlog.js
Meteor.methods({
createLog: function(page){
var timeStamp = Meteor.user().lastActionTimestamp;
//Set variable to store validation if user is logging in
var hasLoggedIn = false;
//Checks if lastActionTimestamp of user is more than an hour ago
if(moment(new Date().getTime()).diff(moment(timeStamp), 'hours') >= 1){
hasLoggedIn = true;
}
console.log("this ran");
var log = {
submitted: new Date().getTime(),
userId: Meteor.userId(),
page: page,
login: hasLoggedIn
}
var logId = Userlogs.insert(log);
Meteor.users.update(Meteor.userId(), {$set: {lastActionTimestamp: log.submitted}});
return logId;
}
});
//router.js This method runs on a filter on every page
'checkLoginStatus': function(page) {
if(Meteor.userId()){
//Logs the page that the user has switched to
Meteor.call('createLog', page);
return page;
}else if(Meteor.loggingIn()) {
return 'loading';
}else {
return 'loginPage';
}
}
However this does not work and it ends up with a recursive creation of userlogs. I believe that this is due to the fact that i did a Collection.find in a router filter method. Does anyone have a work around for this issue?
When you're updating Meteor.users and setting lastActionTimestamp, Meteor.user will be updated and send the invalidation signal to all reactive contexts which depend on it. If Meteor.user is used in a filter, then that filter and all consecutive ones, including checkLoginStatus will rerun, causing a loop.
Best practices that I've found:
Avoid using reactive data sources as much as possible within filters.
Use Meteor.userId() where possible instead of Meteor.user()._id because the former will not trigger an invalidation when an attribute of the user object changes.
Order your filters so that they run with the most frequently updated reactive data source first. For example, if you have a trackPage filter that requires a user, let it run after another filter called requireUser so that you are certain you have a user before you track. Otherwise if you'd track first, check user second then when Meteor.logginIn changes from false to true, you'd track the page again.
This is the main reason we switched to meteor-mini-pages instead of Meteor-Router because it handles reactive data sources much easier. A filter can redirect, and it can stop() the router from running, etc.
Lastly, cmather and others are working on a new router which is a merger of mini-pages and Meteor.Router. It will be called Iron Router and I recommend using it once it's out!