npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@spinajs/orm-cli

v2.0.547

Published

CLI commands for spinajs ORM migrations

Readme

@spinajs/orm-cli

Command line front end for spinajs ORM migrations. Five commands — apply, roll back, report, force a state, scaffold — over the orm.Migration facade in @spinajs/orm.

The package is a thin wrapper on purpose. Everything that decides what a migration run means lives in @spinajs/orm; what lives here is the argument handling, the operator-facing wording and the exit codes. The dependency runs one way only — orm-cli → orm — so the ORM stays usable, and testable, with no CLI in its dependency tree.

Install

npm i @spinajs/orm-cli

The package ships a config fragment that appends its own command directory to system.dirs.cli, which is where @spinajs/cli looks for commands. Installing it is therefore enough — spinajs migrate-status works with no import and no wiring on your side. If your application builds its own command list instead, import '@spinajs/orm-cli' is all that is needed: @Command registers each class in DI the moment the module is evaluated.

The commands are also plain DI classes, so a script can drive them without commander:

import { DI } from '@spinajs/di';
import { MigrateStatusCommand } from '@spinajs/orm-cli';

await (await DI.resolve(MigrateStatusCommand)).execute();

Running a command never migrates anything

Every command starts by resolving an Orm, and an ordinary DI.resolve(Orm) ends with the boot migration pass — every pending migration on every connection whose Migration.OnStartup is on. For an application that is the point. For a migration tool it is a trap, twice over:

  • a connection holding a failed migration refuses every migration run, so the resolve throws before the command body starts. That took down every command on the row it was invoked about, including migrate-resolve — the one command that clears it, and the one the refusal names as the remedy.
  • migrate-status would apply everything pending and only then report, so the deploy gate asking "is this database current?" made it current, answered "yes" and exited 0, with the DDL it was meant to hold back already run.

So the commands resolve their Orm through resolveCliOrm(), which passes MigrateOnStartup: false (an IOrmOptions field of @spinajs/orm). Everything else about resolving happens — connections, models, value converters, orm.Migration — only the boot pass is skipped. It is opt-in: nothing changes for an application that resolves an Orm the ordinary way, and this package ships no configuration that would switch startup migrations off for anybody.

Two consequences worth knowing:

  • migrate-up --fake means what it says on a Migration.OnStartup connection. A boot pass would have really applied the migrations the flag promises only to record.
  • A migration applied by the CLI never gets its data() hook. Seeding belongs to the boot pass: Orm.resolve() seeds what its own startup run applied, and a later boot finds the migration already applied and seeds nothing. That was already true of every connection with Migration.OnStartup off; it is now true of all of them. Migrations that must be seeded have to be applied by an application boot, not by migrate-up.

Commands

| Command | Options | Does | | --- | --- | --- | | migrate-up | -n, --name [name], -c, --connection [connection], -f, --fake | Applies pending migrations on every configured connection | | migrate-down | -n, --name [name], -c, --connection [connection], -a, --all, -f, --fake | Rolls back — the last applied batch only unless --all | | migrate-status | — | Prints one line per migration per connection; the deploy gate | | migrate-resolve | -n, --name [name] (required), --applied, --rolled-back | Records the outcome of a FAILED migration | | migrate-create | -n, --name [name] (required), -d, --dir [dir], -c, --connection [connection] | Scaffolds a migration file |

migrate-up

spinajs migrate-up
spinajs migrate-up --name AddUserTable_2026_07_29_10_00_00
spinajs migrate-up --connection reporting   # this connection only
spinajs migrate-up --fake            # record as applied without running anything

Without --name it applies everything pending, in (timestamp, name) order, across every configured connection. With --name it applies exactly that one.

--connection limits the run to one connection. Every other configured connection is left completely untouched — its migration service is never reached, so its tracking table is not even created. The name is matched against the configured connections (aliases included, since they resolve to the same connection), and one nothing answers to throws rather than running nothing: a filter that silently matched nothing would exit 0 reporting "0 migrations applied".

Two named-run outcomes are deliberately not reported as success:

  • the name matches nothing in the registry — the facade throws rather than returning an empty list, because "0 migrations applied" from a typo is indistinguishable from "already current";
  • the name is registered but the connection it declares is not configured in this deployment. The facade only warns and returns [] there, so this command checks status() afterwards and exits non-zero with an explanation.

migrate-down

spinajs migrate-down                 # the LAST APPLIED BATCH, not everything
spinajs migrate-down --all           # every applied migration, on every connection
spinajs migrate-down --name AddUserTable_2026_07_29_10_00_00
spinajs migrate-down --connection reporting --all   # everything, on one connection

The default scope is the last applied batch — one migrate-up run undone, not the whole history. --all reverses everything. --connection narrows whichever of those two applies, and is announced first for that reason: --all --connection reporting is "every applied migration on one connection". The command says which scope it is about to reverse before it does it, because by the time the result line prints, the schema has already changed.

A rollback drops the tracking row rather than stamping it "rolled back": the table is meant to hold only migrations that are actually present in the database, and both a missing row and a rolled-back one read as pending to the next migrate-up.

--name has a known sharp edge in the migration service: it is handed a one-element unit list, so every other applied row in the target batch looks unmatched and gets warned about as "no registered migration matches them (file deleted or renamed)". Those rows are healthy, and the remedy that warning suggests — removing the row by hand — is destructive here. This command prints a line saying exactly that before the run, so the warnings can be ignored.

migrate-status

spinajs migrate-status
   STATE         BATCH  CONNECTION       MIGRATION
   applied           1  default          AddUserTable_2026_07_29_10_00_00
!! FAILED            0  default          AddOrderIndex_2026_07_29_11_00_00
?? INTERRUPTED       0  default          BackfillTotals_2026_07_29_12_00_00
   pending           -  default          AddInvoices_2026_07_30_09_00_00

Output goes to stdout via console.log, not through the framework logger: it is this command's product, something an operator greps and a script pipes, and routing it through the log would let a configured level or target swallow it.

A failed row carries !! in the leftmost column, not just the word FAILED. That row is the one line in the report that stops every later migrate-up on its connection, and it has to survive being skimmed in a wall of applied. Below the table the command prints the two exact migrate-resolve invocations for each failed migration.

?? marks an interrupted migration — one that was started and never finished, because the process running it was killed before it could record either outcome. It carries the opposite warning to FAILED: it blocks nothing, and the next migrate-up re-runs it from the top, whether or not anybody looked. Under the default Transaction.Mode: None that means non-idempotent data changes get applied twice, silently. The same two migrate-resolve invocations are printed for it. See "Interrupted runs" in the ORM migration docs.

[checksum mismatch] marks a migration whose source changed after it was applied. It is reported but does not on its own make the command exit non-zero — only pending and failed work do.

migrate-resolve

The escape hatch for a run that died halfway. Valid on the two row shapes whose real outcome nobody recorded — failed (FinishedAt NULL and Logs set) and interrupted (StartedAt set, FinishedAt and Logs both NULL). Anything healthy, rolled back or absent is refused rather than silently rewritten.

spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --applied      # the change IS in the database
spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --rolled-back  # the change is NOT

Exactly one of the two flags, never both and never neither: the point of the command is to state which of the two things actually happened, and neither the CLI nor the ORM can find that out on its own. The refusal happens before any Orm is resolved, so a malformed command line never opens a database connection.

--rolled-back makes the migration pending again — it will run on the next migrate-up.

migrate-create

spinajs migrate-create --name AddInvoices
spinajs migrate-create --name AddInvoices --dir ./src/migrations --connection reporting

Prints the path it wrote, on its own line, so $(spinajs migrate-create -n AddInvoices) is usable. Defaults: ./src/migrations and the default connection.

--name takes the prefix only, letters and digits, starting with a letter. The _yyyy_MM_dd_HH_mm_ss suffix is appended here, and it is not decoration: that timestamp is the only ordering the migration runner has, and it is read back out of the class name. A name the runner cannot parse is refused up front, and an existing file is never overwritten.

The generated class only takes effect once it is imported — the @Migration decorator has to run to register it. Re-export it from your package or application index, the way src/migrations/*.ts files are re-exported elsewhere in spinajs.

Exit codes

| Command | 0 | non-zero | | --- | --- | --- | | migrate-up | migrations applied, or nothing was pending | a named run applied nothing because its connection is not configured, or it is still pending/failed; a --connection nothing answers to; any error from the run | | migrate-down | rollback completed, or nothing to roll back | a --connection nothing answers to; any error from the run | | migrate-status | every migration is applied | anything is pending or failed | | migrate-resolve | the state was recorded | both/neither flag given; the row is neither failed nor interrupted | | migrate-create | file written | invalid name or connection; the file already exists |

migrate-status is meant to be a deploy gate — "is this database current?" — so an un-run migration is a "no", not just a failed one.

Two things the table does not say:

  • A 0 from migrate-status means "nothing is pending", not "the database is reachable and configured". With no connections configured, nothing is registered, so nothing is pending and the command exits 0. A gate that must also catch a failed config should check that the command reported migrations at all.
  • Requires a @spinajs/cli that propagates process.exitCode. Earlier versions ended the bin's success path with a bare process.exit(0), which discards whatever a command set — driven through such a bin, migrate-status exits 0 even with pending work. If you are pinned to one, call the command class directly (see the snippet at the top) rather than going through the bin.

The blocking guarantee is best-effort

A failed migration blocks every later migrate-up on its connection. That is what makes migrate-status + migrate-resolve a safe recovery loop instead of a suggestion: a half-applied schema change cannot be built on top of.

The guarantee holds only as far as the bookkeeping does. When a migration fails, the ORM writes the failure into the tracking table — and if that write fails too (the connection dropped, the table is locked), the error is caught and logged rather than raised. The run still fails, but the row that would have blocked the next migrate-up was never written, and the next run proceeds as if nothing had happened.

In practice this needs the database to fail twice, in a specific order. It matters when you are reading logs after an incident: a migrate-up that succeeded shortly after a failed one is not by itself proof that the failure was resolved. Check migrate-status.

Notes

  • Migrations run against a schema no model is wired to yet. Use the OrmDriver passed to up(), never a model class. The data() hook runs later, once models are available.
  • --fake records the outcome without executing anything, on both migrate-up and migrate-down. It is for a database that was changed out of band and needs the tracking table brought in line.
  • migrate-status reports every configured connection, including ones whose Migration.OnStartup is off — hiding those would answer "nothing to see" for exactly the connections somebody is most likely asking about. It has no --connection of its own, for the same reason: the report is the deploy gate, and a gate that can be narrowed is a gate that can be talked past.