nodejs-store

Serverless and connection reuse

The problem

The same models are used from two kinds of process:

The tempting shortcut is to wire the data layer inside the handler: create a driver client, call init(), run the query. That creates a client per invocation, repeats startup work on every request, and — because the library keeps its connection map in module scope — lets concurrent invocations overwrite each other’s routing. It also leaves per-request permission context somewhere a warm container can carry into the next invocation.

Why nodejs-store

Split the responsibilities explicitly:

Because of that split, datasource setup and schema registration belong to process startup; only the permission context is genuinely per request.

Walkthrough

Module scope — done once per process, and once per warm container on serverless:

// store.js
const { MongoClient } = require('mongodb');
const { init, store, executors } = require('nodejs-store');

// Schemas are pure JSON: register at module load (re-registering the same name is fine).
// `datasource` + `namespace` fix the default location; a request can re-target it later.
store.register({
  name: 'Order',
  collection: 'orders',
  datasource: 'cluster',
  namespace: 'app',
  idPrefix: 'OD',
  timestamps: true,
  fields: {
    code: { type: 'string', default: '' },
    status: { type: 'string', default: 'draft' },
    createdBy: { type: 'string' },
  },
  read: ['editor', 'viewer', 'creator'],
  write: ['editor', 'creator'],
});

// Fail-secure once, at startup: from here on a missing context throws instead of passing checks.
store.setRequireContext(true);

// The client and the pool are yours; the library only holds the handles you hand it.
const client = new MongoClient(process.env.MONGO_URL);
const pgPool = createPgPool(); // your pool

let ready = null;
function boot() {
  // Memoised: the first caller connects and inits; later callers reuse the same promise.
  if (!ready) {
    ready = (async () => {
      await client.connect();
      await init({
        cluster: client,                                    // MongoClient: namespace picks the db
        pg: executors.createConnection('postgres', pgPool),
      });
    })();
  }
  return ready;
}

module.exports = { store, boot };

Then the handler only does per-request work:

const { store, boot } = require('./store');

async function handler(req) {
  await boot();                 // no-op after the first invocation in this container

  // Per request: set the context from the authenticated session.
  store.setContext({ userId: req.user.id, roles: req.user.roles });

  return store.query(
    'Order($condition:@c0,$sort:@s0,$limit:@l){ _id, code, status }',
    { c0: { createdBy: req.user.id }, s0: { createdAt: -1 }, l: 20 },
  );
}

Anything that varies per request and is not the context goes through the route override, not through a fresh init():

// Same process, same connections, different tenant — no rebinding of the connection map.
await store.query(
  'Order($condition:@c0){ _id, code }',
  { c0: { status: 'open' } },
  { source: 'cluster', namespace: `tenant_${req.user.tenantId}` },
);

Jobs that must ignore the caller context declare themselves instead of relying on a missing context, and bounded units of work scope their roles with a callback:

// Cron / queue consumer.
await store.runAsInternal(() => store.remove('Order', { status: 'stale' }));

// Temporarily switch roles for one unit of work (nested-safe; restores the outer context).
const preview = await store.scopedRoles(['viewer'], () => store.query(gql, params));

The datasource helpers let a host assert what it configured without touching the driver:

const { datasource } = require('nodejs-store');
datasource.hasConnection('pg');  // is this source in the map?
datasource.isSql('pg');          // true → commands for it go through translate → exec

Pitfalls

See also