Releases: arkstack-hq/arkormx
2.10.2
What's Changed
Fixes
- Casts no longer double-apply after a write.
save(), softdelete(), andrestore()refreshed the model from the returned database row throughfill(), which re-ran the set-cast on values that were already in storage form. For non-idempotent casts (money, arrays) this corrupted the in-memory value — e.g. reading a price immediately afterproduct.fill({ price: 100 }).save()returned10000instead of100, and only re-fetching the row read correctly. Write results are now merged via a raw assignment that skips mutators and casts, so a cast appliessetonce on write andgetonce on read and round-trips correctly throughsave()with no re-fetch.
Features
-
Change existing columns in migrations. Chain
.change()at the end of a column definition insidealterTable()to redefine its type, nullability, default, or (for enums) values in place:schema.alterTable('users', (table) => { table.string('status').default('active').change() table.bigInteger('points').nullable().change() table.enum('role', ['admin', 'editor', 'viewer']).change() })
On the Kysely adapter this emits
ALTER COLUMNstatements (with aUSINGcast) and re-applies a single-column unique constraint; enum value changes recreate the enum type transaction-safely. The Prisma compatibility path rewrites the matchingschema.prismafield line. Note:.change()redefines the full signature (an omitted.default()/.nullable()clears it), and dropping a unique/index is done with explicit operations. -
done(direction)migration hook. Besidesup()anddown(), a migration may define an optionaldone(direction)that runs after its schema operations are applied on the database-backed path (migrate,migrate:fresh,migrate:rollback) — useful for seeding or data backfills once the schema is in place.
Full Changelog: 2.10.0...2.10.2
2.10.0
What's Changed
BREAKING CHANGES
QueryBuilder.delete()now deletes all matching rows and returns the count. Previously it deleted only the first match and returned the hydrated model (ornull). Every row satisfying the query is removed and the number of deleted rows is returned. A where clause is still required (guards against an accidental full-table delete).
deleteOrFail()returns the count too, throwingModelNotFoundExceptiononly when nothing matched.
Changed
- Instance
Model.delete()/Model.forceDelete()no longer refill the instance from the deleted row (they already hold their attributes); they simply mark the model as no longer existing.
Query deletes now route through the adapter's bulk deleteMany, with a select + per-row delete fallback for adapters that don't implement it. - Query deletes now route through the adapter's bulk
deleteMany, with aselect+ per-rowdeletefallback for adapters that don't implement it.
Removed
- Internal
hydrateDeleted,executeDeleteRow, andtryBuildDeleteSpechelpers (no longer used by the new delete path).
Migration
// Before: deleted ONE row, returned the model (or null)
const user = await User.query().where({ active: 0 }).delete()
if (user) console.log('deleted', user.getAttribute('email'))// After: deletes ALL matches, returns the count
const count = await User.query().where({ active: 0 }).delete()
console.log(`deleted ${count} rows`)- If you relied on
delete()/deleteOrFail()returning the deleted model, fetch it first (first()/firstOrFail()) then delete.
If you relied on deleting only one row, add an explicit constraint (e.g. a uniquewhere, orwhereKey(...)).
model.delete()/forceDelete()are unchanged in behavior (still delete that one record); only their internal mechanism changed.
Full Changelog: 2.9.4...2.10.0
2.9.4
What's Changed
Decimal and DateTime column types + a datetime cast
New schema column types on TableBuilder:
table.decimal(name, precision = 8, scale = 2, options?)— a fixed-precision
decimal column. Maps tonumeric(precision, scale)on the Kysely (SQL)
adapter and toDecimal @db.Decimal(precision, scale)in generated Prisma
schema.table.dateTime(name, options?)— a date-time column. Maps totimestamp
(without time zone) on the Kysely adapter and toDateTimein Prisma. This is
distinct fromtimestamp(), which maps totimestamptz.
schema.createTable('invoices', (table) => {
table.id();
table.decimal('amount', 10, 2);
table.dateTime('issuedAt');
});New datetime attribute cast. Reads values into a DateTime instance (from
@h3ravel/support, a dayjs-backed wrapper) for ergonomic date manipulation, and
writes them back as a native Date for persistence:
import { Model } from 'arkormx';
import { DateTime } from '@h3ravel/support';
export class Invoice extends Model {
protected override casts = {
issuedAt: 'datetime',
} as const;
}
const invoice = await Invoice.query().findOrFail(1);
const issuedAt = invoice.getAttribute('issuedAt') as DateTime; // DateTime
invoice.setAttribute('issuedAt', DateTime.now());
await invoice.save(); // persisted as a DateFull Changelog: 2.9.3...2.9.4
2.9.3
What's Changed
Toggle foreign-key constraints when seeding
SchemaBuilder gains static helpers for temporarily disabling PostgreSQL
foreign-key enforcement — useful when seeding interdependent data or inserting
rows in an order that would otherwise violate foreign keys. On PostgreSQL this
switches the connection's session_replication_role between replica
(suppresses FK triggers) and origin (restores them).
SchemaBuilder.withoutForeignKeyConstraints(callback): the recommended
entry point. Runs the callback with constraints disabled and restores them
afterwards, even if the callback throws. The disable, the callback, and the
re-enable run inside a single transaction so they share one connection (the
setting is connection-scoped) and roll back together on failure.SchemaBuilder.disableForeignKeyConstraints()/
SchemaBuilder.enableForeignKeyConstraints(): the underlying toggles,
for finer control. Run them inside a singleDB.transaction(...)so they
share a connection with the work in between.
import { SchemaBuilder } from 'arkormx';
await SchemaBuilder.withoutForeignKeyConstraints(async () => {
await User.factory()
.hasAttached(Tenant.factory().has(Project.factory(3)), { status: 'active' }, 'tenants')
.create();
});Requires a SQL-backed adapter with raw-query support and a database role
permitted to set session_replication_role (typically superuser, or e.g.
rds_superuser on managed PostgreSQL). The Prisma compatibility adapter does
not support it.
Full Changelog: 2.9.2...2.9.3
2.9.2
What's Changed
Fixed
- json/jsonb columns now serialise correctly on
create()andupdate().
Previously, persisting a JS array to ajson-cast column via
Model.create(),query().create(), orquery().update()bound the value as
a Postgres array and failed withinvalid input syntax for type json(objects
were unaffected). These paths bypassed instancesave(), where the cast
serialisation lived. The serialisation is now centralised and applied on the
query-builder insert/update paths, so json casts behave consistently
regardless of how a row is written, on every adapter.
Added
Model.getCasts()— the model's resolved cast map.Model.castAttributesForPersistence(attributes)— applies built-in
persistence casts (json serialisation) to a raw attribute payload.
Full Changelog: 2.9.1...2.9.2
2.9.1
2.9.0
What's Changed
Query builder: JSON, string-matching, and HAVING predicates
The query builder gains a set of new where/having helpers, all also
available on relationship queries.
JSON predicates (PostgreSQL)
Filter JSON/JSONB columns, with a ->-delimited path for nested keys
('meta->preferences->theme'). Each has an orWhere… counterpart.
whereJsonContains/whereJsonDoesntContain— JSONB containment (@>)whereJsonContainsKey/whereJsonDoesntContainKey— key/path existencewhereJsonLength— array-length comparison; accepts(column, value)or(column, operator, value)whereJsonOverlaps— array element overlap
These compile to PostgreSQL JSONB operators on the Kysely adapter and are
rejected by the Prisma compatibility adapter.
String matching
whereNotLike/orWhereNotLikeandorWhereLike— portable (built on
contains/NOT), so they work on both the Kysely and Prisma compatibility adapters.orWhereFullText— OR companion towhereFullText(SQL-backed adapter only).
HAVING clauses
Filter grouped rows after groupBy():
having(column, value)/having(column, operator, value)— column comparison; multiple calls combine with ANDorHaving(...)— OR-combined varianthavingRaw(sql, bindings?)/orHavingRaw(sql, bindings?)— raw clauses for aggregate expressions such ascount(*)
having and friends require a SQL-backed adapter and are rejected by the Prisma
compatibility adapter.
What's Changed
Full Changelog: 2.8.1...2.9.0
2.8.1
What's Changed
🐛 Bug Fixes
ark migrate now loads the user config before resolving the adapter
Adapter-backed migrations run through the configured database adapter instead of silently falling back to the Prisma workflow (the regression that broke adapter projects driven via the Arkstack CLI).
Persisted-metadata-feature validation now runs per migration, in application order, immediately before each apply — instead of building every migration's plan up front. This stops a later migration's up() (e.g. raw SQL for a trigger/constraint referencing an earlier migration's table) from executing before that table exists, fixing from-zero migrations, while still aborting before applying a migration that uses a disabled feature.
Full Changelog: 2.8.0...2.8.1
2.8.0
What's Changed
Active-record persistence helpers
Models and the query builder gain a set of new helpers:
- Find-or helpers (
Model.query()/Model.where(...)):firstOrNew,
firstOrCreate,firstOr,updateOrCreate. - Fail-loud instance methods:
saveOrFail(),updateOrFail(attributes),
deleteOrFail()— each runs in a transaction and rethrows on failure.
Instanceupdate()keeps its silentfalse-on-failure contract. - Change tracking:
getChanges(),getPrevious(key?), and the
wasRecentlyCreatedflag. - Static shortcuts:
Model.all(),Model.where(where),Model.create(data),
Model.upsert(values, uniqueBy, update?),Model.destroy(idOrIds).
exists-driven save semantics ⚠️ behavior change
save() now decides between insert and update based on a new exists flag
rather than whether a primary key is set:
new Model(...)starts withexists === falseand inserts on first save —
even if you assign a primary key yourself (previously this updated).- Models loaded from the database, returned by
create(), or attached through
eager loading haveexists === trueand update on save. - Hard
delete()andforceDelete()resetexiststofalse; soft delete
leaves ittrue.
The new exists property is readable on any model. If you previously relied on
new Model({ id }).save() performing an update, load the record first (or set
exists = true) to keep that behavior.
Collection-loaded models (get(), all()) are now hydrated consistently with
single-record loads: they start clean, so saving an unmodified collection
model is a no-op update instead of rewriting every column.
Full Changelog: 2.7.0...2.8.0
2.7.0
What's Changed
Features
-
SQL join family. The query builder now supports joins on SQL-backed
adapters:join/innerJoin,leftJoin,rightJoin,crossJoin, the
value-comparisonjoinWhere/leftJoinWhere/rightJoinWhere, subquery joins
joinSub/leftJoinSub/rightJoinSub/crossJoinSub, and lateral joins
joinLateral/leftJoinLateral. Pass a closure to build compoundON
conditions through the newJoinClausebuilder (on,orOn,where,
whereNull,onRaw, and nested groups). Available onModel.query()and
DB.table(). -
Configurable migration timestamps.
table.timestamps()now accepts
timestamps(casing?, mapCasing?), where each argument is'camel','snake',
or an explicit{ createdAt?, updatedAt? }object. The first controls the
attribute names; the second controls the mapped physical column names via
.map(). The no-argument default (createdAt/updatedAt) is unchanged. -
Multi-statement raw SQL.
DB.raw()now runs scripts containing more than
one statement, includingdo $$ ... $$blocks. The Kysely adapter splits the
script into individual statements — ignoring semicolons inside string
literals, dollar-quoted bodies, and comments — and executes them sequentially
inside a transaction.
Fixes
whereRawidentifier casing. Bare camelCase identifiers in
whereRaw()/orWhereRaw()are now automatically double-quoted on the Kysely
adapter, sowhereRaw('createdAt < ?', [before])compiles to
"createdAt" < $1instead of folding to a non-existentcreatedatcolumn. SQL
keywords, function names, already-quoted identifiers, and string literals are
left untouched.
Adapters
- New
joinsadapter capability. The Kysely adapter advertises and implements
it; the Prisma compatibility adapter rejects join clauses with a clear
UnsupportedAdapterFeatureException.
Full Changelog: 2.6.1...2.7.0