Prorm instance is one connection to one database. ConnectionManager holdsimport { createConnectionManager, DataTypes } from 'ts-prorm-orm';
const manager = createConnectionManager();
const main = await manager.addConnection('main', {
dialect: 'postgres',
host: 'db.internal', database: 'app',
username: 'app', password: process.env.PG_PASSWORD!,
});
const analytics = await manager.addConnection('analytics', {
dialect: 'clickhouse',
host: 'ch.internal', database: 'events',
});
await main.connect();
await analytics.connect();addConnection constructs the instance but does not connect it — callconnect() yourself, so you control when the sockets open. Adding a connectionsrc/
db/
index.ts # the ConnectionManager, built once
models/
main/ # defined against the 'main' connection
user.ts
order.ts
analytics/
event.ts
migrations/
main/ # prorm migration:run -- --path ./migrations/main
analytics/
.envMAIN_DATABASE_URL=postgres://app:secret@db.internal:5432/app
ANALYTICS_DATABASE_URL=clickhouse://default:secret@ch.internal:8123/eventsimport { createConnectionManager } from 'ts-prorm-orm';
export const manager = createConnectionManager();
export const main = await manager.addConnection('main', {
dialect: 'postgres',
url: process.env.MAIN_DATABASE_URL,
pool: { max: 20 },
});
export const analytics = await manager.addConnection('analytics', {
dialect: 'clickhouse',
url: process.env.ANALYTICS_DATABASE_URL,
});
await Promise.all([main.connect(), analytics.connect()]);$ DATABASE_URL=$MAIN_DATABASE_URL prorm migration:run -- --path ./migrations/main
$ DATABASE_URL=$ANALYTICS_DATABASE_URL prorm migration:run -- --path ./migrations/analytics| Call | Purpose |
|---|---|
addConnection(name, options) | Register a connection; returns the Prorm. |
getConnection(name) | Fetch it, or undefined. |
hasConnection(name) | Existence check. |
getConnectionNames() | Every registered name. |
connectionsMap | A copy of the whole Map. |
removeConnection(name) | Close and forget it; returns whether it existed. |
closeAll() | Close every connection and clear the registry. |
defaultConnection | Get/set the default. |
null. SettingdefaultConnection to a name that is not registered throws.const User = main.define('User', userAttrs, { tableName: 'users' });
const Event = analytics.define('Event', eventAttrs, { tableName: 'events' });
await main.sync();
await analytics.sync();include — an association spans one connection.const activeUsers = await User.findAll({ where: { status: 'active' } });
const events = await analytics
.model('Event')
.findAll({ where: { userId: { [Op.in]: activeUsers.map((u) => u.id) } } });const rows = await manager.query('analytics',
'SELECT day, count(*) FROM events WHERE day >= :from GROUP BY day',
{ replacements: { from: '2026-01-01' } });SELECT resolves to the array of rows, exactly as prorm.query() does onATTACH DATABASEimport { createConnectionManager, DataTypes } from 'ts-prorm-orm';
const manager = createConnectionManager();
const main = await manager.addConnection('main', { dialect: 'sqlite', storage: './main.db' });
const analytics = await manager.addConnection('analytics', { dialect: 'sqlite', storage: './analytics.db' });
await main.connect();
await analytics.connect();
const User = main.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING },
}, { tableName: 'users' });
const Visit = analytics.define('Visit', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
userId: { type: DataTypes.INTEGER },
path: { type: DataTypes.STRING },
}, { tableName: 'visits' });
await main.sync();
await analytics.sync();
await User.bulkCreate([{ name: 'Ada' }, { name: 'Linus' }]);
await Visit.bulkCreate([{ userId: 1, path: '/' }, { userId: 1, path: '/docs' }, { userId: 2, path: '/' }]);
await manager.attachDatabase('main', 'analytics', './analytics.db');
const rows = await manager.query('main', `
SELECT u.name, count(v.id) AS visits
FROM users u JOIN analytics.visits v ON v.userId = u.id
GROUP BY u.name ORDER BY u.name`);
console.log(rows); // [ { name: 'Ada', visits: 2 }, { name: 'Linus', visits: 1 } ]
await manager.detachDatabase('main', 'analytics');
await manager.closeAll();^[A-Za-z_][A-Za-z0-9_]*$ because it sits in anusing: '<connection name>' routes a single query to another registeredawait User.findAll({ where: { status: 'active' }, using: 'analytics' });addConnection() registers the manager on each Prorm instance it creates, sousingConnection 'nope' not found. Available: main, analytics
Model 'User' is not defined on connection 'analytics'. Defined there: Eventconst Event = manager.getConnection('analytics')!.model('Event')!;
await Event.findAll({ where: { userId: 42 } });[!WARNING] A transaction belongs to one connection
There is no two-phase commit across connections, so a "transaction"
spanning two databases can leave one committed and the other rolled back.
await main.transaction(async (t) => {
await Order.create(order, { transaction: t });
// Do NOT write to `analytics` here expecting atomicity.
});ReplacingMergeTree on ClickHouse).import { DataTypes, Op } from 'ts-prorm-orm';
// main and analytics: the connections exported by src/db/index.ts above.
const Order = main.define('Order', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
total: { type: DataTypes.DECIMAL(12, 2) },
}, { tableName: 'orders' });
const Outbox = main.define('Outbox', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
event: { type: DataTypes.STRING(64) },
payload: { type: DataTypes.JSON },
sentAt: { type: DataTypes.DATE, allowNull: true },
}, { tableName: 'outbox' });
const Event = analytics.define('Event', {
id: { type: DataTypes.STRING(64), primaryKey: true }, // the idempotency key
name: { type: DataTypes.STRING(64) },
payload: { type: DataTypes.JSON },
}, { tableName: 'events', timestamps: false });
// In the request: both rows commit, or neither does.
export async function placeOrder(total: number) {
return main.transaction(async (t) => {
const order = await Order.create({ total }, { transaction: t });
await Outbox.create(
{ event: 'order.placed', payload: { orderId: order.get('id'), total } },
{ transaction: t },
);
return order;
});
}
// In the worker: at-least-once delivery to the second database.
export async function drainOutbox() {
const pending = await Outbox.findAll({ where: { sentAt: { [Op.is]: null } }, limit: 100 });
for (const row of pending) {
await Event.create({ id: `outbox-${row.get('id')}`, name: row.get('event'), payload: row.get('payload') });
await row.update({ sentAt: new Date() });
}
}process.on('SIGTERM', async () => {
await manager.closeAll();
});