Skip to content

Releases: wrsouza/orion

v0.4.2

11 Jun 00:29

Choose a tag to compare

Bug Fixes

Static pagination and iteration methods on Model

The five pagination and iteration methods documented in the API were only reachable through the query builder (e.g. User.where(...).paginate()). Calling them directly on a model subclass — as shown in the documentation — threw a TypeScript error (ts(2339)). They are now available as static methods on every model:

// All of these now work directly on the model class
const page   = await User.paginate(15);        // Paginator<User>
const page   = await User.simplePaginate(20);  // SimplePaginator<User>

await User.chunk(200, async (users) => { /* ... */ });

for await (const user of User.cursor()) { /* ... */ }
for await (const user of User.lazy(500)) { /* ... */ }

All five methods accept the same arguments as their ModelBuilder equivalents and delegate directly to them.


New Features

ModelNotFoundException

findOrFail() and firstOrFail() now throw ModelNotFoundException instead of a generic Error. Import and catch it by type:

import { ModelNotFoundException } from '@wrsouza/orion';

try {
  const user = await User.findOrFail(99);
} catch (e) {
  if (e instanceof ModelNotFoundException) {
    // e.message → "[orion] No query results for model [User] with key 99."
  }
}

MassAssignmentException

When strict mass assignment is enabled (Model.preventSilentlyDiscardingAttributes()), attempting to fill a non-fillable attribute now throws MassAssignmentException instead of a generic Error:

import { Model, MassAssignmentException } from '@wrsouza/orion';

Model.preventSilentlyDiscardingAttributes();

try {
  await User.create({ name: 'Alice', role: 'admin' }); // 'role' not in @fillable
} catch (e) {
  if (e instanceof MassAssignmentException) {
    // e.message → "[orion] Add [role] to fillable on [User] to allow mass assignment."
  }
}

QueryException

All query execution failures (unique constraint violations, foreign key errors, connection drops, etc.) are now wrapped in a QueryException that exposes the SQL, bindings, and original driver error for structured error handling:

import { QueryException } from '@wrsouza/orion';

try {
  await User.create({ email: 'duplicate@example.com' });
} catch (e) {
  if (e instanceof QueryException) {
    console.log(e.sql);           // INSERT INTO "users" ...
    console.log(e.bindings);      // ['duplicate@example.com']
    console.log(e.cause.message); // duplicate key value violates unique constraint ...

    // Detect specific violations via driver error codes
    const isUnique = (e.cause as any).code === '23505'; // PostgreSQL
  }
}

Migration Guide

No breaking changes. All existing code continues to work.

  • If you were catching Error from findOrFail() or firstOrFail(), switch to ModelNotFoundException for a precise match — the generic Error catch will still work but is less specific.
  • QueryException extends Error, so existing generic catch blocks are unaffected.

Documentation

  • New Error Handling page covering all three exception classes with framework-specific examples for Express, Fastify, NestJS, Next.js, and React Router.
  • Fixed paginate(perPage, page) argument order in all integration examples — the arguments were shown in the wrong order in Express, Fastify, NestJS, Next.js, and React Router docs.
  • Added static method examples for paginate, simplePaginate, chunk, cursor, and lazy to the Pagination page.

v0.4.1

10 Jun 03:03

Choose a tag to compare

Bug Fixes

  • CLI: Fixed compatibility with NestJS (and other projects using moduleResolution: nodenext) — the CLI ts-node bootstrap now explicitly overrides moduleResolution to node and disables resolvePackageJsonExports, preventing TS5110 and TS5096 errors when running orion migrate, migrate:rollback, etc.

v0.4.0

10 Jun 01:35

Choose a tag to compare

What's new

Cast enum

Replaces raw string literals in cast declarations with named constants — full IDE autocomplete and no risk of typos:

import { Cast } from '@wrsouza/orion';

// before
@casts({ isActive: 'boolean', bornAt: 'date', price: 'decimal:2' })

// now
@casts({ isActive: Cast.Boolean, bornAt: Cast.Date, price: Cast.Decimal(2) })

Available values: Cast.Number, Cast.String, Cast.Boolean, Cast.Json, Cast.Date, Cast.Array, Cast.Hashed, Cast.Encrypted, Cast.EncryptedArray, Cast.EncryptedJson, Cast.ImmutableDate, Cast.ImmutableDatetime, Cast.JsonUnicode, Cast.AsStringable, Cast.Decimal(n).


@cast() — property decorator

Declare the cast directly on the field, alongside @map():

export class User extends Model {
  @map('is_active')
  @cast(Cast.Boolean)
  declare isActive: boolean;

  @map('born_at')
  @cast(Cast.Date)
  declare bornAt: Date;

  @cast(MoneyCast)   // custom cast class also accepted
  declare balance: Money;
}

Property-level declarations are merged on top of class-level @casts({}) — the property decorator wins on conflict.


@hidden() — property decorator

@hidden now works as a property decorator in addition to the existing class decorator form:

// class (unchanged)
@hidden(['password', 'remember_token'])
class User extends Model {}

// property (new)
class User extends Model {
  @hidden()
  declare password: string;
}

v0.3.3

09 Jun 03:46

Choose a tag to compare

Added

BelongsToMany.withoutPivot() — hides the pivot object during JSON serialization without removing it from the runtime.

categories(): BelongsToMany<Category> {
  return this.belongsToMany(Category, 'category_post', 'post_id', 'category_id')
             .withoutPivot();
}

Fixed

toArray() now respects toJSON() on non-Model relationship values — objects like PivotRecord stored in _relations will have their toJSON() called during serialization; if it returns undefined, the key is omitted from the output.

v0.3.2

09 Jun 03:19

Choose a tag to compare

Bug Fixes

BelongsToMany eager loading always returned an empty array.

When using Model.with(['relation']) on many-to-many relationships, the returned collection was always empty even when there were existing records in the pivot table.

Root Cause: The eager load flow executed get() before match(). The get() method moved the column pivot_ from _attributes into a PivotRecord (and deleted it from _attributes). When match() subsequently executed, it attempted to read attributes['pivot'] — which was already undefined — and therefore no model could be associated with its parent.

Correction: match() now reads the FK directly from the PivotRecord:

// antes
const fk = (related as any)._attributes[`pivot_${this.foreignPivotKey}`]; // undefined

// depois
const pivot = (related as any)._relations[this._pivotAlias] as PivotRecord;
const fk = pivot?.get(this.foreignPivotKey); // ✓

v0.3.1

09 Jun 01:00

Choose a tag to compare

Bug Fixes
@Map decorator not applied on Model.create() (#commit)

When using Model.create() or factories with @map-decorated properties (e.g. @Map('published_at') declare publishedAt), the camelCase property name was being sent directly to the database instead of the mapped snake_case column name, causing errors like:

ERROR: column "publishedAt" of relation "posts" does not exist
Root cause: newInstance() copied attributes directly into _attributes without going through fill(), which is the only path that applied the columnMap translation.

Fix: newInstance() now translates attribute keys through columnMap before storing them, so { publishedAt: value } correctly becomes { published_at: value } in _attributes before the INSERT.

Tests
Added regression test: @Map decorator with Model.create — verifies that camelCase keys passed to Model.create() are correctly stored and retrieved using their mapped column names.

v0.3.0

08 Jun 16:58

Choose a tag to compare

What's Changed

@uuid() decorator — new preferred way to declare UUID primary keys

Replace the HasUuids(Model) mixin with the @uuid() property decorator:

// before
class User extends HasUuids(Model) {
  declare id: string;
}

// after
class User extends Model {
  @uuid()
  declare id: string;
}
  • Sets incrementing: false and keyType: 'string' automatically
  • Apply to multiple fields to generate UUIDs for extra columns
  • HasUuids and HasUlids are still available for advanced use (custom generator via newUniqueId())

@table() now accepts a plain string

// before
@table({ name: 'users', primaryKey: 'id', incrementing: false, keyType: 'string' })

// after
@table('users')

Pass an object only when you need extra options (primaryKey, timestamps, connection, etc.).

@fillable removed

TypeScript's type system enforces mass-assignment at compile time — a runtime allowlist adds no safety in a typed codebase. Remove @fillable and @guarded from your models. Use @hidden to exclude sensitive fields from JSON serialization.

@map() formatting

The @map('column_name') decorator now sits on its own line above declare:

@map('created_at')
declare createdAt: Date;

Breaking changes

  • @fillable and @guarded are no longer exported. Remove them from your models and imports.
  • HasUuids(Model) pattern is superseded by @uuid(). HasUuids remains exported for backwards compatibility but is no longer documented.

v0.2.1

08 Jun 02:39

Choose a tag to compare

Fixed

  • Lazy-load database driversmysql2, mariadb, mssql and
    better-sqlite3 are now loaded only when a connection of that type is
    actually created. Previously, importing the ORM caused all drivers to be
    required at startup, resulting in Cannot find module 'mysql2/promise'
    for projects that only installed pg.

Upgrade

npm install @wrsouza/orion@0.2.1

v0.2.0

08 Jun 02:02

Choose a tag to compare

What's Changed

Centralised configuration — createConnection()

  • New createConnection(config: OrionConfig) as the single entry point — bootstraps connection, morph map and lazy-loading guard in one call
  • Accepts connection as a URL string or a full config object
  • CLI auto-detects src/database.ts, database.ts, src/orion.ts in addition to legacy orion.config.* files
  • --config <path> flag for non-standard locations

CLI improvements

  • CLI auto-registers ts-node from the project's node_modulesnpx orion migrate works without wrapper scripts
  • db:seed [--class=Name] — run seeders (falls back to alphabetical order when no DatabaseSeeder is found)
  • make:seed <name> — scaffold a seeder file
  • make:factory <name> — scaffold a factory file

Seeds

  • Seeder abstract base class with run() and call([...seeders]) for chaining
  • Seeder and factory paths derived from the migrations path (same src/database/ base)

@map('column_name') decorator

  • Maps a model property to a different DB column name (camelCase ↔ snake_case)
  • Transparent read/write, where/orderBy/groupBy, serialization and dirty tracking all use the property name

.primary() column modifier

  • Fluent inline PRIMARY KEY on any column: table.uuid('id').primary()
  • Supported in all five grammar dialects

foreignId / foreignUuid fluent FK chaining

  • Both methods now create the column and return a ForeignKeyDefinition for chaining:
    table.foreignUuid('user_id').references('id').on('users').onDelete('CASCADE')

Model.with() static shortcut

  • User.with('posts').get() works directly
  • ModelBuilder.first() now applies eager loads (previously only get() did)

Migration transactions

  • Each up() and down() runs inside a database transaction — partial failures are fully rolled back

Bug fixes

  • ModelSubclass index signature removed — no more ts(2684) in strict TypeScript
  • OrmConfig in CLI now supports all five drivers (previously only postgres)
  • make:migration templates now import from @wrsouza/orion (was orion)
  • Husky hooks fixed for Windows (missing #!/bin/sh shebang)

Breaking changes

  • optionalDependencies replaced by peerDependencies + peerDependenciesMeta — install only the driver you need: npm install pg