JCC Express

Database

TypeORM

Introduction

JCC Express MVC supports TypeORM as an ORM option via DB_ORM=typeorm. TypeORM uses decorator-based entities in app/Entities/ — separate from JCC Eloquent models in app/Models/.

You can also use TypeORM alongside JCC Eloquent by keeping DB_ORM=jcc and configuring database.typeorm.dataSource in app/Config/database.ts (the framework still registers a singleton DataSource).


Enable TypeORM

Option A — TypeORM as primary ORM

env
DB_ORM=typeorm
DB_CONNECTION=mysql2
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=root
DB_PASSWORD=password

# Development only — use migrations in production
TYPEORM_SYNCHRONIZE=false
TYPEORM_LOGGING=false

DB_CONNECTION is mapped to a TypeORM driver:

DB_CONNECTIONTypeORM type
mysql2, mysql, mariadbmysql
postgres, postgresql, pgpostgres
sqlite, better-sqlite3better-sqlite3

Option B — TypeORM alongside JCC Eloquent

Keep DB_ORM=jcc for Knex/JCC models and ensure app/Config/database.ts includes:

TypeScript
import { TypeORMDataSource } from "@/Services/TypeORMDataSource";

export const database = {
  orm: config.get("DB_ORM", "jcc"),
  typeorm: {
    dataSource: TypeORMDataSource,
    autoloadEntities: true,
    synchronize: false,
    logging: false,
  },
  // ...
};

TypeORMServiceProvider registers the client whenever database.typeorm.dataSource is set.


Required packages

Bash
npm install typeorm

Install the driver for your database (already common in JCC apps):

Bash
npm install mysql2    # MySQL / MariaDB
npm install pg        # PostgreSQL
npm install better-sqlite3   # SQLite

App DataSource class

app/Services/TypeORMDataSource.ts extends TypeORM DataSource and reads connection settings from DB_* env vars:

TypeScript
import { DataSource } from "typeorm";
import { typeORMOptionsFromEnv } from "jcc-express-mvc/lib/Database/Drivers/TypeORM/connection";
import type { TypeORMDataSourceOptions } from "jcc-express-mvc/lib/Database/Drivers/TypeORM/types";

export class TypeORMDataSource extends DataSource {
  constructor(options?: TypeORMDataSourceOptions) {
    super(typeORMOptionsFromEnv(options));
  }
}

Wire it in app/Config/database.ts:

TypeScript
typeorm: {
  dataSource: TypeORMDataSource,
  autoloadEntities: true,
  synchronize: config.get("TYPEORM_SYNCHRONIZE", "false") === "true",
  logging: config.get("TYPEORM_LOGGING", "false") === "true",
},

When the driver connects, it scans app/Entities/ for .ts/.js files and registers exported entity classes (unless autoloadEntities: false or you pass entities explicitly).


Define entities

Place entities in app/Entities/ with TypeORM decorators (@Entity, @Column, relations, etc.). See Extend BaseEntity for the recommended User entity shape used in this project.

Generate a new entity with ArtisanNode:

Bash
bun artisanNode make:model Product

When DB_ORM=typeorm, make:model scaffolds into app/Entities/ with TypeORM decorators and extends BaseEntity.


Entities do not have to extend TypeORM's BaseEntity — a plain @Entity() class works with getRepository(Entity). Extending BaseEntity is recommended when you want:

  • Active Record-style static methodsfind, findOne, findOneBy, save, remove, and related helpers on the class itself.
  • Route model binding — the framework resolves route parameters into loaded entity instances when your controller action is typed with the entity class (see below and Routing).

Example (app/Entities/User.ts):

TypeScript
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToMany,
  BaseEntity,
} from "typeorm";
import { Post } from "./Post";

@Entity({ name: "users" })
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ unique: true })
  email!: string;

  @Column({ nullable: true })
  name?: string;

  @Column({ select: false })
  password!: string;

  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];
}

Static methods

With BaseEntity, query without injecting the DataSource in every call:

TypeScript
import { User } from "@/Entities/User";

// List
const users = await User.find({ relations: { posts: true } });

// Find by primary key
const user = await User.findOneBy({ id: 1 });

// Find by another column
const byEmail = await User.findOneBy({ email: "jane@example.com" });

// Persist changes on an instance
user.name = "Jane";
await user.save();

You can still use typeorm().getRepository(User) or constructor-injected TypeORMDataSource when you prefer the repository pattern.

Static methods and the data source: TypeORM's BaseEntity static helpers need a data source on the entity class. After the app boots, call User.useDataSource(typeorm()) once per entity (for example in AppServiceProvider.boot()). If you skip that step, use typeorm().getRepository(User) instead — route model binding still works either way.

TypeScript
// app/Providers/AppServiceProvider.ts (boot)
import { User } from "@/Entities/User";
import { Post } from "@/Entities/Post";

async boot() {
  const ds = this.app.resolve("typeorm");
  if (ds?.isInitialized) {
    User.useDataSource(ds);
    Post.useDataSource(ds);
  }
}

Route model binding

Register a route whose parameter name matches the camelCase entity name (User{user}):

TypeScript
Route.get("/users/{user}", [UsersController, "show"]);

Type-hint the entity on a @Method() action. The framework loads the record via the TypeORM repository (findOne by id, or by column with {column$user} syntax):

TypeScript
import { Inject, Method } from "jcc-express-mvc/Core/Dependency";
import { User } from "@/Entities/User";

@Inject()
export class UsersController {
  @Method({ params: ["user"] })
  async show(user: User) {
    return user;
  }
}

Binding by email (or any column):

TypeScript
Route.get("/users/{email$user}", [UsersController, "showByEmail"]);
TypeScript
@Method({ params: ["user"] })
async showByEmail(user: User) {
  return user;
}

The resolved value is a BaseEntity instance, so you can call instance methods (save(), remove(), etc.) directly in the action.


Using TypeORM in controllers

Constructor injection (recommended):

TypeScript
import { Inject } from "jcc-express-mvc/Core/Dependency";
import { TypeORMDataSource } from "@/Services/TypeORMDataSource";
import { User } from "@/Entities/User";

@Inject()
export class UsersController {
  constructor(private readonly db: TypeORMDataSource) {}

  async index() {
    return this.db.getRepository(User).find();
  }

  async show(id: number) {
    return this.db.getRepository(User).findOne({
      where: { id },
      relations: { posts: true },
    });
  }
}

Global helper:

TypeScript
import { User } from "@/Entities/User";

const users = await typeorm().getRepository(User).find();

Auth and route model binding

When DB_ORM=typeorm, framework helpers resolve users and route-bound models via TypeORM repositories:

  • findUserById, findUserForAuth
  • resolveModelBinding — implicit {user} / :user binding and {column$user} column binding
  • AuthMiddleware API auth lookup

getModel("User") loads from app/Entities/User instead of app/Models/.

For route model binding, extend BaseEntity on your entity class and type-hint the entity in @Method() actions (see Extend BaseEntity above). The demo route GET /api/typeorm/{user} in this repo uses that pattern.


Migrations (TypeORM CLI)

Use the TypeORM CLI directly for migrations (not yet wrapped in ArtisanNode):

Bash
# Generate a migration from entity changes
npx typeorm migration:generate -d app/Services/TypeORMDataSource.ts migrations/Init

# Run migrations
npx typeorm migration:run -d app/Services/TypeORMDataSource.ts

For local prototyping only, TYPEORM_SYNCHRONIZE=true auto-syncs schema from entities. Do not use synchronize in production.


How the framework registers TypeORM

When DB_ORM=typeorm (or when database.typeorm.dataSource is set in app/Config/database.ts):

  • Your TypeORMDataSource is registered as a singleton.
  • Entities in app/Entities/ are autoloaded on connect (unless disabled in config).
  • The data source is available via constructor injection, app.resolve("typeorm"), or the global typeorm() helper.
  • Knex / Sequelize / Mongoose setup is skipped so TypeORM owns the connection.
  • Prisma and TypeORM do not register together when one is the primary DB_ORM.
  • The database monitor query collector is Knex-only; it is skipped when DB_ORM is not jcc.

Demo routes

This repo includes TestTypeORMController with routes under /api/typeorm when TypeORM is configured:

  • GET /api/typeorm — list users with posts
  • POST /api/typeorm — create sample user and post
  • GET /api/typeorm/{user} — show one user via route model binding