Releases: wrsouza/orion
v0.4.2
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
ErrorfromfindOrFail()orfirstOrFail(), switch toModelNotFoundExceptionfor a precise match — the genericErrorcatch will still work but is less specific. QueryExceptionextendsError, 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, andlazyto the Pagination page.
v0.4.1
Bug Fixes
- CLI: Fixed compatibility with NestJS (and other projects using
moduleResolution: nodenext) — the CLI ts-node bootstrap now explicitly overridesmoduleResolutiontonodeand disablesresolvePackageJsonExports, preventingTS5110andTS5096errors when runningorion migrate,migrate:rollback, etc.
v0.4.0
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
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
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
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
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: falseandkeyType: 'string'automatically - Apply to multiple fields to generate UUIDs for extra columns
HasUuidsandHasUlidsare still available for advanced use (custom generator vianewUniqueId())
@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
@fillableand@guardedare no longer exported. Remove them from your models and imports.HasUuids(Model)pattern is superseded by@uuid().HasUuidsremains exported for backwards compatibility but is no longer documented.
v0.2.1
Fixed
- Lazy-load database drivers —
mysql2,mariadb,mssqland
better-sqlite3are 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 inCannot find module 'mysql2/promise'
for projects that only installedpg.
Upgrade
npm install @wrsouza/orion@0.2.1v0.2.0
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
connectionas a URL string or a full config object - CLI auto-detects
src/database.ts,database.ts,src/orion.tsin addition to legacyorion.config.*files --config <path>flag for non-standard locations
CLI improvements
- CLI auto-registers
ts-nodefrom the project'snode_modules—npx orion migrateworks without wrapper scripts db:seed [--class=Name]— run seeders (falls back to alphabetical order when noDatabaseSeederis found)make:seed <name>— scaffold a seeder filemake:factory <name>— scaffold a factory file
Seeds
Seederabstract base class withrun()andcall([...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
ForeignKeyDefinitionfor chaining:
table.foreignUuid('user_id').references('id').on('users').onDelete('CASCADE')
Model.with() static shortcut
User.with('posts').get()works directlyModelBuilder.first()now applies eager loads (previously onlyget()did)
Migration transactions
- Each
up()anddown()runs inside a database transaction — partial failures are fully rolled back
Bug fixes
ModelSubclassindex signature removed — no morets(2684)in strict TypeScriptOrmConfigin CLI now supports all five drivers (previously onlypostgres)make:migrationtemplates now import from@wrsouza/orion(wasorion)- Husky hooks fixed for Windows (missing
#!/bin/shshebang)
Breaking changes
optionalDependenciesreplaced bypeerDependencies+peerDependenciesMeta— install only the driver you need:npm install pg