Skip to content

Releases: arkstack-hq/arkormx

2.10.2

25 Jun 14:50

Choose a tag to compare

What's Changed

Fixes

  • Casts no longer double-apply after a write. save(), soft delete(), and restore() refreshed the model from the returned database row through fill(), 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 after product.fill({ price: 100 }).save() returned 10000 instead of 100, 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 applies set once on write and get once on read and round-trips correctly through save() with no re-fetch.

Features

  • Change existing columns in migrations. Chain .change() at the end of a column definition inside alterTable() 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 COLUMN statements (with a USING cast) and re-applies a single-column unique constraint; enum value changes recreate the enum type transaction-safely. The Prisma compatibility path rewrites the matching schema.prisma field 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. Besides up() and down(), a migration may define an optional done(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

25 Jun 01:33

Choose a tag to compare

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 (or null). 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, throwing ModelNotFoundException only 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 a select + per-row delete fallback for adapters that don't implement it.

Removed

  • Internal hydrateDeleted, executeDeleteRow, and tryBuildDeleteSpec helpers (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 unique where, or whereKey(...)).
    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

23 Jun 06:20

Choose a tag to compare

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 to numeric(precision, scale) on the Kysely (SQL)
    adapter and to Decimal @db.Decimal(precision, scale) in generated Prisma
    schema.
  • table.dateTime(name, options?) — a date-time column. Maps to timestamp
    (without time zone) on the Kysely adapter and to DateTime in Prisma. This is
    distinct from timestamp(), which maps to timestamptz.
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 Date

Full Changelog: 2.9.3...2.9.4

2.9.3

21 Jun 15:57

Choose a tag to compare

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 single DB.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

20 Jun 23:04

Choose a tag to compare

What's Changed

Fixed

  • json/jsonb columns now serialise correctly on create() and update().
    Previously, persisting a JS array to a json-cast column via
    Model.create(), query().create(), or query().update() bound the value as
    a Postgres array and failed with invalid input syntax for type json (objects
    were unaffected). These paths bypassed instance save(), 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

19 Jun 04:49

Choose a tag to compare

What's Changed

  • feat(query): support callbacks in where/orWhere for nested groups by @3m1n3nc3 in #8

Full Changelog: 2.9.0...2.9.1

2.9.0

19 Jun 03:26
9317cb5

Choose a tag to compare

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 existence
  • whereJsonLength — 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 / orWhereNotLike and orWhereLike — portable (built on
    contains/NOT), so they work on both the Kysely and Prisma compatibility adapters.
  • orWhereFullText — OR companion to whereFullText (SQL-backed adapter only).

HAVING clauses

Filter grouped rows after groupBy():

  • having(column, value) / having(column, operator, value) — column comparison; multiple calls combine with AND
  • orHaving(...) — OR-combined variant
  • havingRaw(sql, bindings?) / orHavingRaw(sql, bindings?) — raw clauses for aggregate expressions such as count(*)

having and friends require a SQL-backed adapter and are rejected by the Prisma
compatibility adapter.

What's Changed

  • Feat/json like having where helpers by @3m1n3nc3 in #7

Full Changelog: 2.8.1...2.9.0

2.8.1

16 Jun 09:51

Choose a tag to compare

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

16 Jun 01:38

Choose a tag to compare

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.
    Instance update() keeps its silent false-on-failure contract.
  • Change tracking: getChanges(), getPrevious(key?), and the
    wasRecentlyCreated flag.
  • 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 with exists === false and 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 have exists === true and update on save.
  • Hard delete() and forceDelete() reset exists to false; soft delete
    leaves it true.

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

15 Jun 10:16

Choose a tag to compare

What's Changed

Features

  • SQL join family. The query builder now supports joins on SQL-backed
    adapters: join/innerJoin, leftJoin, rightJoin, crossJoin, the
    value-comparison joinWhere/leftJoinWhere/rightJoinWhere, subquery joins
    joinSub/leftJoinSub/rightJoinSub/crossJoinSub, and lateral joins
    joinLateral/leftJoinLateral. Pass a closure to build compound ON
    conditions through the new JoinClause builder (on, orOn, where,
    whereNull, onRaw, and nested groups). Available on Model.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, including do $$ ... $$ 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

  • whereRaw identifier casing. Bare camelCase identifiers in
    whereRaw()/orWhereRaw() are now automatically double-quoted on the Kysely
    adapter, so whereRaw('createdAt < ?', [before]) compiles to
    "createdAt" < $1 instead of folding to a non-existent createdat column. SQL
    keywords, function names, already-quoted identifiers, and string literals are
    left untouched.

Adapters

  • New joins adapter 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