Modern ORM and query builder

OBJX keeps SQL close, types sharp, and runtime power inside the project.

Inspired by Objection.js, rebuilt around an embedded SQL engine, typed graph operations, official SQLite/Postgres/MySQL drivers, codegen, plugins, and a production-ready NestJS module.

  • Typed models with defineModel
  • createSnakeCaseNamingPlugin() for camelCase over snake_case schemas
  • createSnakeCaseNamingStrategy() for session-wide physical naming
  • Embedded SQL compiler and runtime
  • insertGraph, upsertGraph, eager loading
  • Plugins for tenant scope, soft delete, audit trail, timestamps

Control

Raw SQL stays first-class. You can drop to sql, ref, and compiled queries without fighting the ORM.

Graph runtime

Use eager loading, insertGraph, upsertGraph, relate, and unrelate with the same session model.

Ambient context

ExecutionContextManager carries transaction scope, tenant ids, actor ids, request metadata, and plugin state.

Operational tooling

Real migrations, seeds, code generation, starter templates, validation adapters, and framework integration already ship as packages.

Install

Choose the happy path you want to ship.

Switch between the main installation presets. The rest of the API stays the same.

SQLite happy path

Typed models

Define models with defaults, generated columns, and relation metadata.

OBJX treats schema metadata as runtime truth and type signal at the same time. Defaults can be static or factory-based, generated fields stay out of required insert payloads, relation columns stay explicit, and naming plugins can keep models in camelCase even when the database stays in snake_case.

  • col.bigint() and col.bigInt() for bigint columns
  • .default(value) and .default(() => value)
  • .generated() for runtime-populated fields like tenant ids and graph-owned foreign keys
  • createSnakeCaseNamingPlugin() remaps physical columns during model definition
  • createSnakeCaseNamingStrategy() keeps physical naming configurable per session
  • belongsToOne, hasOne, hasMany, manyToMany
Model definition
import { col, defineModel, hasMany } from '@qbobjx/core';
import { createSnakeCaseNamingPlugin } from '@qbobjx/plugins';

const generateSnowflakeId = () => BigInt(Date.now());

export const Project = defineModel({
  name: 'Project',
  table: 'projects',
  columns: {
    id: col.bigInt().primary().default(() => generateSnowflakeId()),
    tenantId: col.text().generated(),
    name: col.text(),
    status: col.text().default('planned'),
    deletedAt: col.timestamp().nullable(),
  },
  relations: (project) => ({
    tasks: hasMany(() => Task, {
      from: project.columns.id,
      to: Task.columns.projectId,
    }),
  }),
  plugins: [createSnakeCaseNamingPlugin()],
});
Runtime and transactions
const rows = await executionContextManager.run(
  {
    values: {
      tenantId: 'demo',
      actorId: 'user_admin',
    },
  },
  () =>
    session.transaction(async (trx) => {
      await trx.execute(
        Project.insert({
          name: 'Ship docs',
          status: 'planned',
        }),
      );

      return trx.execute(
        Project.query()
          .where(({ status }, op) => op.eq(status, 'planned'))
          .withRelated({
            tasks: true,
          }),
        { hydrate: true },
      );
    }),
);

Sessions

createSqliteSession, createPostgresSession, and createMySqlSession expose the same execution model.

Transactions

Every official driver supports session.transaction(...) and nested transactions where savepoints are available.

Hydration

Dates, booleans, json payloads, and bigint values can be hydrated automatically from query results.

Plugins

Register plugins globally in the session or per model to compose tenant scope, audit, validation, timestamps, and soft delete behavior.

Operational workflow

Migrations, seeds, and codegen are part of the stack.

The recommended path is explicit migrations and seeds. Use templates to scaffold the folders, then run the same commands across SQLite, Postgres, or MySQL.

1. Scaffold

Generate starter migration and seed schemas.

2. Migrate

Run up and down versions explicitly per dialect.

3. Seed

Load initial data and revert it with tracked seed history.

4. Generate

Introspect and generate model output when you need schema-driven bootstrapping.

Typed migrations and seeds
npm run codegen -- template --template migration-seed-schemas --out ./db

npm run codegen -- migrate --dialect postgres --database "$DATABASE_URL" --dir ./db/migrations --direction up

npm run codegen -- seed --dialect postgres --database "$DATABASE_URL" --dir ./db/seeds --direction run

The recommended app shape

  • db/migrations/*.migration.mjs
  • db/seeds/*.seed.mjs
  • npm run db:migrate before app start
  • npm run db:seed for dev and demo data
  • API examples keep camelCase models while migrations define snake_case columns
  • NestJS example already follows this flow end to end

Framework integration

Use the official NestJS module instead of hand-wiring providers.

@qbobjx/nestjs ships a dynamic module, session injection helpers, request-context interception, validation mapping, and shutdown hooks for drivers that need cleanup.

  • ObjxModule.forRoot(...) and ObjxModule.forRootAsync(...)
  • @InjectObjxSession() for clean session injection
  • automatic request context from headers like x-tenant-id
  • global filter for ObjxValidationError
NestJS module setup
@Module({
  imports: [
    ObjxModule.forRootAsync({
      global: true,
      inject: [AuditTrailStore],
      useFactory: (auditTrailStore) => ({
        session: createSqliteSession({
          driver: createSqliteDriver({
            databasePath: './data/app.sqlite',
            pragmas: ['foreign_keys = on'],
          }),
          executionContextManager: createExecutionContextManager(),
          hydrateByDefault: true,
          plugins: [
            createTenantScopePlugin(),
            createSoftDeletePlugin(),
            createAuditTrailPlugin({
              actorKey: 'actorId',
              emit: (entry) => auditTrailStore.append(entry),
            }),
          ],
        }),
        requestContext: {
          enabled: true,
        },
      }),
    }),
  ],
})
export class AppModule {}

Repository examples

Use working apps instead of theory.

The repository already includes end-to-end examples for runtime behavior, APIs, and tooling.

Packages

Published modules by responsibility.

Install only what you need or compose the whole stack.

GitHub Pages

Static docs site with no build step.

This documentation lives directly in the pages/ folder and is deployed by a dedicated GitHub Actions workflow.

  1. Enable GitHub Pages in the repository and choose GitHub Actions as the source.
  2. Edit files inside pages/.
  3. Push to main or master.
  4. The workflow uploads pages/ and deploys it as the site artifact.