Skip to Content
HeronJS v4 is now available.
v4MongoDB & CQL

MongoDB, Cassandra, and ScyllaDB

Install mongodb for MongoDB, or cassandra-driver for Cassandra/ScyllaDB. Drivers load at connection time. Their selected feature entrypoints expose typed hooks and need that SDK’s declarations when typechecking; core/common do not. Native instances are available only through disposable useMongo/useCql leases.

Models and named dependency injection

import {z} from 'zod'; import {Module, DataSource} from '@heronjs/common'; import {MongoDataSource, MongoSource, Document, Field, Index} from '@heronjs/core/mongodb'; @Document({name: 'records', schema: z.object({id: z.string(), label: z.string()})}) @Index({fields: {label: 1}}) class RecordModel { @Field({name: '_id'}) id = ''; label = ''; } @MongoSource({name: 'records', uri: 'mongodb://127.0.0.1:27017', database: 'records', profile: 'main', revision: '1', models: [RecordModel]}) class RecordsSource extends MongoDataSource {} class RecordsRepository { constructor(@DataSource('records') private readonly source: RecordsSource) {} find(id: string) { return this.source.repository(RecordModel).get({id}); } } @Module({databases: [RecordsSource], providers: [RecordsRepository]}) class RecordsModule {}

Heron awaits datasource initialization before dependent providers initialize. Named, default, and class tokens resolve to the same owned singleton; application shutdown disposes it once. Outside an application, construct the source, await initialize(), and call dispose() in finally. Constructors and decorators perform no network I/O or DDL.

Each source explicitly lists its models. Model schemas bind the model’s TypeScript shape to runtime validation. Field supplies MongoDB storage aliases; Index supplies migration metadata. BSON values such as ObjectId, Decimal128, and Long can be retained with appropriate Zod instance schemas. There is no JSON round trip to coerce them. Optional ModelCodec<T> encodes and decodes storage documents; schemas still validate its results. Storage aliases must be unique.

Repositories expose get, find, insert, update, and remove. Reads return undefined when absent. Invalid models fail before writes. Updates validate the complete merged model and detect concurrent changes; retry decisions remain explicit. Basic Mongo criteria use field equality, including literal object equality; query operators, aggregation, transactions, streams, and driver-specific options use the native API. Mongo find uses a bounded offset continuation, sorted by _id; pages are not snapshots during concurrent mutations. Limits are 1–1000, default 100.

CQL models

import {z} from 'zod'; import {CqlSource, CqlDataSource, Table, Column, PartitionKey, ClusteringKey} from '@heronjs/core/cql'; @Table({name: 'records', schema: z.object({tenant: z.string(), id: z.string(), label: z.string()})}) class CqlRecord { @Column({type: 'text'}) @PartitionKey(0) tenant = ''; @Column({type: 'text'}) @ClusteringKey(0) id = ''; @Column({name: 'display_label', type: 'text'}) label = ''; } @CqlSource({ name: 'records', backend: 'cassandra', contactPoints: ['127.0.0.1'], localDataCenter: 'datacenter1', keyspace: 'records', profile: 'main', revision: '1', models: [CqlRecord], configure: () => ({options: {socketOptions: {readTimeout: 5000}}}), }) class CqlRecordsSource extends CqlDataSource {}

Use backend: 'scylladb' for ScyllaDB with the same driver integration. Keyspace creation is an explicit deployment operation. Every column needs a CQL type; partition and clustering positions must be unique. get/update/remove require the complete primary key. find requires the partition key and accepts a contiguous clustering-key prefix, never implicit ALLOW FILTERING. Paging uses the native opaque page state; do not accept page states for a different query. Primary key changes require an explicit delete/insert migration.

The implementation reuses the driver’s mapper for CRUD and uses prepared CQL for pagination. Use native Uuid/Long/BigDecimal types with schema validation when precision matters; do not convert 64-bit integers to JavaScript numbers. Counter, collection-specific, conditional, and other advanced operations remain available through native callbacks.

Tenant profiles, ownership, and leases

resolveTenant(tenant) may return a promise of trusted connection configuration. Resolve identifiers against an application-controlled registry; never accept a client-supplied connection string, credentials, database name, or keyspace directly. A profile identifies a credential/TLS configuration and revision changes when that configuration rotates.

Mongo pools are keyed by profile, revision, URI, and setup callback identity. Database views share the client within that profile. CQL pools additionally include the keyspace and never execute USE. Keep a setup callback stable for a configuration; creating a new callback on every tenant resolution intentionally creates a separate profile. A supplied CQL client must already use the resolved keyspace.

source.withTenant(tenant, async context => ...) pins one resolved namespace and lease through the callback. Obtain repositories with context.repository(Model), await all operations inside it, and do not retain the repository afterward. A regular source.repository(Model, tenant) resolves the tenant afresh per operation, allowing revision rotation between operations.

Default limits are 20 clients, 5-minute idle retention, 10-second connection deadline, and 30-second shutdown drain. Pending connections count against capacity and concurrent opens for one profile share one promise. Active leases cannot be evicted. Capacity pressure replaces an idle client or throws CAPACITY when all are active. Cleanup attempts every owned client. Timed-out clients that complete later are closed.

// Available globally after startup when the datasource feature is loaded. await using connection = await mongo(source, tenant); const cursor = connection.value.database.collection('events').find({kind: 'example'}); try { for await (const event of cursor) await processEvent(event); } finally { await cursor.close(); } // For a CQL datasource, await using connection = await cql(source, tenant).

CQL offers the analogous callback and lease functions. Native callbacks must complete all work before returning; use an explicit lease when a cursor, stream, or transaction outlives one call. Release is idempotent. After the shutdown drain deadline, remaining owned clients close; borrowed clients remain caller-owned.

The configure: () => ({options, createClient, client, owned}) setup callback provides SDK options, a client factory, or a supplied client when a new connection is acquired. Supplied clients default to owned: false; use owned: true only when transferring closure responsibility. Factory-created clients are owned by default. Drivers handle their own reconnection and supported retry policies; Heron adds no query replay. Error codes distinguish missing driver, configuration, readiness, closure, capacity, timeout, model, migration, and lock failures while preserving causes.

Worker processes need their own datasource instance and connection. In a worker definition’s async setup(container), create and await the source, register it as an owned instance and register named aliases, then inject its class/token into tasks. Do not pass parent-process clients through worker messages. The optional thread runtime does not start until configured.

Explicit migrations

mongoSchemaPlan(Model) returns an editable collection/index plan; cqlSchemaPlan(Model) returns editable CQL statements. Inspect and commit the desired schema change with its migration. Decorator changes never alter a live schema automatically.

import type {Migration} from '@heronjs/core/mongodb'; const migrations: Migration[] = [ { id: '001-records', checksum: 'sha256-of-reviewed-migration-content', up: context => context.createModel(RecordModel), down: context => context.dropModel('records'), }, ]; await source.migrate(migrations); // explicitly invoked by deployment tooling

lease.value.migrate(migrations) from useMongo and useCql provides SDK clients plus an abort signal for data backfills or custom schema statements. Migrations have ordered unique IDs and explicit immutable checksums. A namespace-scoped renewable lock serializes execution; checksummed history is written after success. A changed applied checksum fails preflight. Down migrations run in reverse order and require explicit callbacks. History and locks use __heron_migrations in the selected namespace.

Migration callbacks must honor cancellation and be resumable: database operations are not automatically rolled back if a process dies or a lease expires. CQL DDL is not transactional. Review destructive operations and partition-key changes in migration code. Run each tenant rollout explicitly with a trusted tenant list; initialization and tenant discovery do not create schemas or launch migrations.

See database testing  for live-server, restart, authentication/TLS, and SDK-isolation checks. Native MongoDB logging is routed through the strict framework logger for framework-created clients; application-owned clients and listeners must apply the same policy. MongoDB logging reference , CQL mapper reference .

Based on the 4.1.0 development source .

Last updated on