Using TypeORM
TypeORM Overview
TypeORM is a popular TypeScript ORM.
It is based on the idea of creating Entity
classes to represent each database table, with the persistent and join key fields marked with decorators.
Once entity classes are defined, TypeORM provides methods for storing, updating, and querying the entities via the EntityManager
.
DBOS supports TypeORM as an alternative to Knex for transactional data management.
Usage
DBOS supports essentially direct use of TypeORM, but a few additional steps are necessary to inform DBOS about the TypeORM entity list, manage database schemas, and use a transaction in the context of a workflow.
Getting Started
An easy way to get started with TypeORM is to bootstrap your application with our TypeORM template. This is similar to the template used in the quickstart, but built with TypeORM instead of Knex. To download it, run:
npx -y @dbos-inc/create@latest -t hello-typeorm -n <app-name>
Then, build it, run schema migrations, and start the TypeORM sample app:
npm run build
npx dbos migrate
npx dbos start
To see that it's working, visit this URL in your browser: http://localhost:3000/greeting/dbos. You should get this message: Greeting 1: Hello, dbos!
Each time you refresh the page, the counter should go up by one.
Setting Up Entities
In DBOS, TypeORM entities are defined in the same way as any other TypeORM project, for example:
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity()
export class KV {
@PrimaryColumn()
id: string = "t";
@Column()
value: string = "v";
}
DBOS handles the entity registration that would otherwise be done in a TypeORM DataSource
instantiation or configuration file. To make DBOS aware of the entities, a class-level decorator is used on each class containing DBOS transaction methods:
@OrmEntities([KV])
class KVOperations {
}
Schema Management
In production scenarios or when using DBOS Cloud, we strongly recommend you manage your database schema using migrations. TypeORM provides rich native migration support, with documentation here.
You can create a new migration by running:
npx typeorm migration:create migrations/<migration-name>
TypeORM can also automatically generate migration files from changes to your entity files. This requires a TypeORM datasource file, which is included in our template and documented below.
npx typeorm migration:generate -d dist/datasource.js migrations/<migration-name>
This automatically generates a migration file containing commands to transition your database from its current schema to the schema defined in your entity files.
Invoking Transactions
In TypeORM (and many other frameworks), the pattern is to run transactions as callback functions. (This allows the framework to ensure that the transaction is opened and closed properly, and to ensure that all statements run on the same connection from the connection pool.)
DBOS provides a wrapper around TypeORM's transaction functionality so that its workflow state can be kept consistent with the application database.
First, DBOS transactions are declared. The easiest way is with a class method decorated with @Transaction
, and the first argument will be a TransactionContext
with an EntityManager
named client
inside.
@OrmEntities([KV])
class KVOperations {
@Transaction()
static async writeTxn(txnCtxt: TransactionContext<EntityManager>, id: string, value: string) {
const kv: KV = new KV();
kv.id = id;
kv.value = value;
const res = await txnCtxt.client.save(kv);
return res.id;
}
// eslint-disable-next-line @typescript-eslint/require-await
@Transaction({ readOnly: true })
static async readTxn(txnCtxt: TransactionContext<EntityManager>, id: string) {
const kvp = await txnCtxt.client.findOneBy(KV, {id: id});
return kvp?.value || "<Not Found>";
}
}
If preferred, it is possible to define a type
to clean up the transaction method prototypes a little bit.
type TypeORMTransactionContext = TransactionContext<EntityManager>;
Configuring TypeORM
If you are using the TypeORM template, this is done for you.
To enable TypeORM, you must set the app_db_client
field in the DBOS configuration file to typeorm
.
You should also configure TypeORM migration commands.
Here is an example of a configuration file set up for TypeORM:
language: node
database:
hostname: 'localhost'
port: 5432
username: 'postgres'
app_db_name: 'hello_typeorm'
password: ${PGPASSWORD}
connectionTimeoutMillis: 3000
app_db_client: typeorm
migrate:
- npx typeorm migration:run -d dist/datasource.js
rollback:
- npx typeorm migration:revert -d dist/datasource.js
runtimeConfig:
entrypoints:
- dist/src/operations.js
Many TypeORM commands, such as those for schema migration, require a TypeORM datasource file. To avoid managing your configuration in two places, we recommend this file use your DBOS configuration file as a source. Here is an example of a datasource file that does this:
import { parseConfigFile } from '@dbos-inc/dbos-sdk/dist/src/dbos-runtime/config';
import { TlsOptions } from 'tls';
import { DataSource } from "typeorm";
const [dbosConfig, ] = parseConfigFile();
const AppDataSource = new DataSource({
type: 'postgres',
host: dbosConfig.poolConfig.host,
port: dbosConfig.poolConfig.port,
username: dbosConfig.poolConfig.user,
password: dbosConfig.poolConfig.password as string,
database: dbosConfig.poolConfig.database,
ssl: dbosConfig.poolConfig.ssl as TlsOptions,
entities: ['dist/entities/*.js'],
migrations: ['dist/migrations/*.js'],
});
AppDataSource.initialize()
.then(() => {
console.log("Data Source has been initialized!");
})
.catch((err) => {
console.error("Error during Data Source initialization", err);
});
export default AppDataSource;
When referencing this file in commands, use the compiled JavaScript (dist/datasource.js
) instead of the original TypeScript source (datasource.ts
).