Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@objectstack/driver-mongodb

MongoDB driver for ObjectStack — native document database support via the official MongoDB Node.js driver.

⚠️ Single-tenant only

This driver has no row-level tenant isolation: it ignores DriverOptions.tenantId, so reads carry no tenant predicate and writes are not stamped with a tenant column. Rather than serve a multi-tenant deployment without isolation, it refuses to start in one — see Multi-tenancy below. Use @objectstack/driver-sql (PostgreSQL / MySQL / SQLite) for multi-tenant deployments.

Installation

pnpm add @objectstack/driver-mongodb mongodb

Configuration

Option A — Env vars (recommended for objectstack dev / objectstack serve)

export OS_DATABASE_URL=mongodb://localhost:27017/myapp
pnpm dev

The ObjectStack CLI auto-registers this driver when OS_DATABASE_URL starts with mongodb:// or mongodb+srv://. You do not need to set OS_DATABASE_DRIVER separately — the URL scheme is enough. Set it only as an explicit override (recognised values: mongodb, mongo).

Option B — Programmatic via defineStack

import { defineStack } from '@objectstack/spec';
import { MongoDBDriver } from '@objectstack/driver-mongodb';

export default defineStack({
  driver: new MongoDBDriver({
    url: 'mongodb://localhost:27017/myapp',
    database: 'myapp',        // Optional: overrides URI database
    maxPoolSize: 10,          // Optional: connection pool size (default: 10)
    minPoolSize: 1,           // Optional: minimum pool (default: 1)
    connectTimeoutMS: 10000,  // Optional: connection timeout
  }),
});

Configuration Options

Option Type Default Description
url string required MongoDB connection URI
database string from URI Database name
maxPoolSize number 10 Max connection pool size
minPoolSize number 1 Min connection pool size
connectTimeoutMS number 10000 Connection timeout (ms)
serverSelectionTimeoutMS number 5000 Server selection timeout (ms)
options MongoClientOptions {} Additional MongoClient options

Features

Capabilities

Category Capability Supported
CRUD create, read, update, delete
Bulk bulkCreate, bulkUpdate, bulkDelete
Query filters, sorting, pagination, aggregations
Transactions Multi-document transactions ✅ (requires replica set)
Streaming Cursor-based async iteration
Schema Collection + index sync
Advanced Full-text search, JSON queries, geospatial
Joins Cross-collection joins ($lookup)
Window Functions ROW_NUMBER, RANK, etc.
Multi-tenancy Row-level tenant isolation ❌ (boots single-tenant only)

Multi-tenancy

The driver is a single-tenant / embedded driver. Unlike @objectstack/driver-sql — which resolves a tenant column per object, injects a WHERE tenant_field = ? predicate on reads and stamps that column on writes — this driver implements none of that layer. In a multi-tenant deployment every query would read, update and delete other tenants' documents.

So instead of running unisolated, it fails fast at startup (#3724):

Signal Where it is checked Result
Tenancy posture is not singleOS_TENANCY_POSTURE=group|isolated, or derived from OS_MULTI_ORG_ENABLED=true new MongoDBDriver(), re-checked in connect() before a socket is opened throws MongoDBMultiTenantUnsupportedError
An object declares tenancy.enabled: true syncSchema() / syncSchemasBatch() throws, naming every offending object

The error carries code === 'MONGODB_MULTI_TENANT_UNSUPPORTED' so a host can recognise it without matching on the message:

import {
  MongoDBMultiTenantUnsupportedError,
  MULTI_TENANT_UNSUPPORTED_CODE,
} from '@objectstack/driver-mongodb';

There is deliberately no override flag — one would restore exactly the silent cross-tenant access the guard exists to prevent. To run MongoDB, keep the deployment single-tenant (OS_TENANCY_POSTURE=single or unset, OS_MULTI_ORG_ENABLED unset or false, no object declaring tenancy.enabled: true); to go multi-tenant, switch to @objectstack/driver-sql.

ID Handling

ObjectStack uses string IDs (nanoid). The driver:

  • Stores id as a regular string field with a unique index
  • Auto-generates id via nanoid if not provided
  • Never exposes MongoDB's internal _id field in results

Filter Operators

All ObjectStack filter operators are supported:

// MongoDB-style filters (pass-through)
await driver.find('task', {
  where: {
    status: { $in: ['active', 'pending'] },
    priority: { $gte: 3 },
    title: { $contains: 'urgent' },
    deleted_at: { $null: true },
  }
});

// Legacy array-style filters
await driver.find('task', {
  where: [['status', '=', 'active'], 'or', ['priority', '>=', 5]]
});

Transactions

Transactions require a MongoDB replica set (including single-node replica sets for development).

const session = await driver.beginTransaction();
try {
  await driver.create('order', { total: 100 }, { transaction: session });
  await driver.update('inventory', itemId, { stock: newStock }, { transaction: session });
  await driver.commit(session);
} catch (error) {
  await driver.rollback(session);
  throw error;
}

Schema Sync

Schema sync creates collections and indexes:

Field-level unique fields index themselves; everything else is declared in the object's indexes[] — the one surface an index is declared on (a field-level indexed flag is not a FieldSchema key and never built an index, #2377 / #6810).

A lookup field that declares reference also indexes itself, as idx_FIELD_lookup, for join performance; so does a user field. A lookup with no reference does not — there is no declared target to join to.

⚠️ Upgrading: this join index is newer than the driver. The first syncSchema after the upgrade builds it across collections that already hold data, which costs real IO and time on large ones. See CHANGELOG.md for the operational detail. Later boots are no-ops.

await driver.syncSchema('account', {
  name: 'account',
  fields: {
    name: { type: 'string', unique: true },
    email: { type: 'email' },
    company_id: { type: 'lookup', reference: 'company' },
  },
  indexes: [{ fields: ['email'] }],
});
// Creates: idx_id_unique, idx_created_at, idx_updated_at,
//          idx_name_unique, idx_company_id_lookup, idx_email

Aggregation

const results = await driver.aggregate('order', {
  where: { status: 'completed' },
  aggregations: [
    { function: 'sum', field: 'amount', alias: 'total_revenue' },
    { function: 'count', alias: 'order_count' },
  ],
  groupBy: ['region'],
});

Plugin Usage

Register as an ObjectStack plugin:

import mongodbPlugin from '@objectstack/driver-mongodb';

// The plugin registers automatically via onEnable
kernel.use(mongodbPlugin, {
  url: 'mongodb://localhost:27017/myapp',
});

Development

# Run tests (the suites that need a real mongod SKIP — see below)
pnpm test

# Run every suite, including the ones that need a real mongod
OS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1 pnpm test

# Build
pnpm build

The mongod-backed suites are opt-in (#5517)

Seven suites here need a real MongoDB, which mongodb-memory-server provides by downloading a ~123 MB binary on first use. With a cold cache, two vitest workers downloaded it at the same time and the loser's rename failed as an unhandled rejection — turning an all-green run into exit 1 and ejecting unrelated PRs from the merge queue. Those suites are therefore gated behind OS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1: without it they skip, each printing one line that names this issue and the switch, and no download starts.

The rest of the package's tests — filter translation, the shared filter-logic conformance case-set over the emitted documents, sort specs, tenancy guard, temporal helpers — run by default and need no binary. The gate lives in src/test-mongod.ts, which documents the mechanism and what a default run gives up.

License

Apache-2.0. See LICENSING.md.