@spinajs/orm-cli 2.0.490 → 2.0.494
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -247
- package/lib/cjs/cli/MigrateCreate.js +24 -24
- package/lib/mjs/cli/MigrateCreate.js +24 -24
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +70 -68
package/README.md
CHANGED
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
# @spinajs/orm-cli
|
|
2
|
-
|
|
3
|
-
Command line front end for spinajs ORM migrations. Five commands — apply, roll back, report,
|
|
4
|
-
force a state, scaffold — over the `orm.Migration` facade in `@spinajs/orm`.
|
|
5
|
-
|
|
6
|
-
The package is a thin wrapper on purpose. Everything that decides what a migration run means
|
|
7
|
-
lives in `@spinajs/orm`; what lives here is the argument handling, the operator-facing wording
|
|
8
|
-
and the exit codes. The dependency runs one way only — `orm-cli` → `orm` — so the ORM stays
|
|
9
|
-
usable, and testable, with no CLI in its dependency tree.
|
|
10
|
-
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm i @spinajs/orm-cli
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The package ships a config fragment that appends its own command directory to
|
|
18
|
-
`system.dirs.cli`, which is where `@spinajs/cli` looks for commands. Installing it is therefore
|
|
19
|
-
enough — `spinajs migrate-status` works with no import and no wiring on your side. If your
|
|
20
|
-
application builds its own command list instead, `import '@spinajs/orm-cli'` is all that is
|
|
21
|
-
needed: `@Command` registers each class in DI the moment the module is evaluated.
|
|
22
|
-
|
|
23
|
-
The commands are also plain DI classes, so a script can drive them without commander:
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { DI } from '@spinajs/di';
|
|
27
|
-
import { MigrateStatusCommand } from '@spinajs/orm-cli';
|
|
28
|
-
|
|
29
|
-
await (await DI.resolve(MigrateStatusCommand)).execute();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Running a command never migrates anything
|
|
33
|
-
|
|
34
|
-
Every command starts by resolving an `Orm`, and an ordinary `DI.resolve(Orm)` ends with the boot
|
|
35
|
-
migration pass — every pending migration on every connection whose `Migration.OnStartup` is on.
|
|
36
|
-
For an application that is the point. For a migration tool it is a trap, twice over:
|
|
37
|
-
|
|
38
|
-
- a connection holding a **failed** migration refuses every migration run, so the resolve throws
|
|
39
|
-
before the command body starts. That took down every command on the row it was invoked about,
|
|
40
|
-
including `migrate-resolve` — the one command that clears it, and the one the refusal names as
|
|
41
|
-
the remedy.
|
|
42
|
-
- `migrate-status` would apply everything pending and only then report, so the deploy gate asking
|
|
43
|
-
"is this database current?" made it current, answered "yes" and exited `0`, with the DDL it was
|
|
44
|
-
meant to hold back already run.
|
|
45
|
-
|
|
46
|
-
So the commands resolve their Orm through `resolveCliOrm()`, which passes `MigrateOnStartup:
|
|
47
|
-
false` (an `IOrmOptions` field of `@spinajs/orm`). Everything else about resolving happens —
|
|
48
|
-
connections, models, value converters, `orm.Migration` — only the boot pass is skipped. It is
|
|
49
|
-
opt-**in**: nothing changes for an application that resolves an Orm the ordinary way, and this
|
|
50
|
-
package ships no configuration that would switch startup migrations off for anybody.
|
|
51
|
-
|
|
52
|
-
Two consequences worth knowing:
|
|
53
|
-
|
|
54
|
-
- `migrate-up --fake` means what it says on a `Migration.OnStartup` connection. A boot pass would
|
|
55
|
-
have really applied the migrations the flag promises only to record.
|
|
56
|
-
- **A migration applied by the CLI never gets its `data()` hook.** Seeding belongs to the boot
|
|
57
|
-
pass: `Orm.resolve()` seeds what its own startup run applied, and a later boot finds the
|
|
58
|
-
migration already applied and seeds nothing. That was already true of every connection with
|
|
59
|
-
`Migration.OnStartup` off; it is now true of all of them. Migrations that must be seeded have to
|
|
60
|
-
be applied by an application boot, not by `migrate-up`.
|
|
61
|
-
|
|
62
|
-
## Commands
|
|
63
|
-
|
|
64
|
-
| Command | Options | Does |
|
|
65
|
-
| --- | --- | --- |
|
|
66
|
-
| `migrate-up` | `-n, --name [name]`, `-c, --connection [connection]`, `-f, --fake` | Applies pending migrations on every configured connection |
|
|
67
|
-
| `migrate-down` | `-n, --name [name]`, `-c, --connection [connection]`, `-a, --all`, `-f, --fake` | Rolls back — **the last applied batch only** unless `--all` |
|
|
68
|
-
| `migrate-status` | — | Prints one line per migration per connection; the deploy gate |
|
|
69
|
-
| `migrate-resolve` | `-n, --name [name]` (required), `--applied`, `--rolled-back` | Records the outcome of a FAILED migration |
|
|
70
|
-
| `migrate-create` | `-n, --name [name]` (required), `-d, --dir [dir]`, `-c, --connection [connection]` | Scaffolds a migration file |
|
|
71
|
-
|
|
72
|
-
### `migrate-up`
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
spinajs migrate-up
|
|
76
|
-
spinajs migrate-up --name AddUserTable_2026_07_29_10_00_00
|
|
77
|
-
spinajs migrate-up --connection reporting # this connection only
|
|
78
|
-
spinajs migrate-up --fake # record as applied without running anything
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Without `--name` it applies everything pending, in `(timestamp, name)` order, across every
|
|
82
|
-
configured connection. With `--name` it applies exactly that one.
|
|
83
|
-
|
|
84
|
-
`--connection` limits the run to one connection. Every other configured connection is left
|
|
85
|
-
completely untouched — its migration service is never reached, so its tracking table is not even
|
|
86
|
-
created. The name is matched against the configured connections (aliases included, since they
|
|
87
|
-
resolve to the same connection), and one nothing answers to **throws** rather than running
|
|
88
|
-
nothing: a filter that silently matched nothing would exit `0` reporting "0 migrations applied".
|
|
89
|
-
|
|
90
|
-
Two named-run outcomes are deliberately **not** reported as success:
|
|
91
|
-
|
|
92
|
-
- the name matches nothing in the registry — the facade throws rather than returning an empty
|
|
93
|
-
list, because "0 migrations applied" from a typo is indistinguishable from "already current";
|
|
94
|
-
- the name is registered but the connection it declares is not configured in this deployment.
|
|
95
|
-
The facade only warns and returns `[]` there, so this command checks `status()` afterwards and
|
|
96
|
-
exits non-zero with an explanation.
|
|
97
|
-
|
|
98
|
-
### `migrate-down`
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
spinajs migrate-down # the LAST APPLIED BATCH, not everything
|
|
102
|
-
spinajs migrate-down --all # every applied migration, on every connection
|
|
103
|
-
spinajs migrate-down --name AddUserTable_2026_07_29_10_00_00
|
|
104
|
-
spinajs migrate-down --connection reporting --all # everything, on one connection
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
The default scope is the last applied batch — one `migrate-up` run undone, not the whole
|
|
108
|
-
history. `--all` reverses everything. `--connection` narrows whichever of those two applies, and
|
|
109
|
-
is announced first for that reason: `--all --connection reporting` is "every applied migration on
|
|
110
|
-
*one* connection". The command says which scope it is about to reverse *before* it does it,
|
|
111
|
-
because by the time the result line prints, the schema has already changed.
|
|
112
|
-
|
|
113
|
-
A rollback drops the tracking row rather than stamping it "rolled back": the table is meant to
|
|
114
|
-
hold only migrations that are actually present in the database, and both a missing row and a
|
|
115
|
-
rolled-back one read as pending to the next `migrate-up`.
|
|
116
|
-
|
|
117
|
-
`--name` has a known sharp edge in the migration service: it is handed a one-element unit list,
|
|
118
|
-
so every *other* applied row in the target batch looks unmatched and gets warned about as
|
|
119
|
-
"no registered migration matches them (file deleted or renamed)". Those rows are healthy, and
|
|
120
|
-
the remedy that warning suggests — removing the row by hand — is destructive here. This command
|
|
121
|
-
prints a line saying exactly that before the run, so the warnings can be ignored.
|
|
122
|
-
|
|
123
|
-
### `migrate-status`
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
spinajs migrate-status
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
STATE BATCH CONNECTION MIGRATION
|
|
131
|
-
applied 1 default AddUserTable_2026_07_29_10_00_00
|
|
132
|
-
!! FAILED 0 default AddOrderIndex_2026_07_29_11_00_00
|
|
133
|
-
?? INTERRUPTED 0 default BackfillTotals_2026_07_29_12_00_00
|
|
134
|
-
pending - default AddInvoices_2026_07_30_09_00_00
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Output goes to stdout via `console.log`, not through the framework logger: it is this command's
|
|
138
|
-
*product*, something an operator greps and a script pipes, and routing it through the log would
|
|
139
|
-
let a configured level or target swallow it.
|
|
140
|
-
|
|
141
|
-
A failed row carries `!!` in the leftmost column, not just the word `FAILED`. That row is the
|
|
142
|
-
one line in the report that stops every later `migrate-up` on its connection, and it has to
|
|
143
|
-
survive being skimmed in a wall of `applied`. Below the table the command prints the two exact
|
|
144
|
-
`migrate-resolve` invocations for each failed migration.
|
|
145
|
-
|
|
146
|
-
`??` marks an **interrupted** migration — one that was started and never finished, because the
|
|
147
|
-
process running it was killed before it could record either outcome. It carries the opposite
|
|
148
|
-
warning to `FAILED`: it blocks nothing, and the next `migrate-up` re-runs it from the top, whether
|
|
149
|
-
or not anybody looked. Under the default `Transaction.Mode: None` that means non-idempotent data
|
|
150
|
-
changes get applied twice, silently. The same two `migrate-resolve` invocations are printed for
|
|
151
|
-
it. See "Interrupted runs" in
|
|
152
|
-
[the ORM migration docs](../orm/docs/10-schema-and-migrations.md#interrupted-runs).
|
|
153
|
-
|
|
154
|
-
`[checksum mismatch]` marks a migration whose source changed after it was applied. It is
|
|
155
|
-
reported but does **not** on its own make the command exit non-zero — only pending and failed
|
|
156
|
-
work do.
|
|
157
|
-
|
|
158
|
-
### `migrate-resolve`
|
|
159
|
-
|
|
160
|
-
The escape hatch for a run that died halfway. Valid on the two row shapes whose real outcome
|
|
161
|
-
nobody recorded — **failed** (`FinishedAt` NULL and `Logs` set) and **interrupted** (`StartedAt`
|
|
162
|
-
set, `FinishedAt` and `Logs` both NULL). Anything healthy, rolled back or absent is refused rather
|
|
163
|
-
than silently rewritten.
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --applied # the change IS in the database
|
|
167
|
-
spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --rolled-back # the change is NOT
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Exactly one of the two flags, never both and never neither: the point of the command is to state
|
|
171
|
-
which of the two things actually happened, and neither the CLI nor the ORM can find that out on
|
|
172
|
-
its own. The refusal happens before any Orm is resolved, so a malformed command line never opens
|
|
173
|
-
a database connection.
|
|
174
|
-
|
|
175
|
-
`--rolled-back` makes the migration pending again — it *will* run on the next `migrate-up`.
|
|
176
|
-
|
|
177
|
-
### `migrate-create`
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
spinajs migrate-create --name AddInvoices
|
|
181
|
-
spinajs migrate-create --name AddInvoices --dir ./src/migrations --connection reporting
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Prints the path it wrote, on its own line, so `$(spinajs migrate-create -n AddInvoices)` is
|
|
185
|
-
usable. Defaults: `./src/migrations` and the `default` connection.
|
|
186
|
-
|
|
187
|
-
`--name` takes the *prefix* only, letters and digits, starting with a letter. The
|
|
188
|
-
`_yyyy_MM_dd_HH_mm_ss` suffix is appended here, and it is not decoration: that timestamp is the
|
|
189
|
-
only ordering the migration runner has, and it is read back out of the class name. A name the
|
|
190
|
-
runner cannot parse is refused up front, and an existing file is never overwritten.
|
|
191
|
-
|
|
192
|
-
The generated class only takes effect once it is *imported* — the `@Migration` decorator has to
|
|
193
|
-
run to register it. Re-export it from your package or application index, the way `src/migrations/*.ts`
|
|
194
|
-
files are re-exported elsewhere in spinajs.
|
|
195
|
-
|
|
196
|
-
## Exit codes
|
|
197
|
-
|
|
198
|
-
| Command | `0` | non-zero |
|
|
199
|
-
| --- | --- | --- |
|
|
200
|
-
| `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 |
|
|
201
|
-
| `migrate-down` | rollback completed, or nothing to roll back | a `--connection` nothing answers to; any error from the run |
|
|
202
|
-
| `migrate-status` | every migration is applied | anything is pending or failed |
|
|
203
|
-
| `migrate-resolve` | the state was recorded | both/neither flag given; the row is neither failed nor interrupted |
|
|
204
|
-
| `migrate-create` | file written | invalid name or connection; the file already exists |
|
|
205
|
-
|
|
206
|
-
`migrate-status` is meant to be a deploy gate — "is this database current?" — so an un-run
|
|
207
|
-
migration is a "no", not just a failed one.
|
|
208
|
-
|
|
209
|
-
Two things the table does not say:
|
|
210
|
-
|
|
211
|
-
- **A `0` from `migrate-status` means "nothing is pending", not "the database is reachable and
|
|
212
|
-
configured".** With no connections configured, nothing is registered, so nothing is pending and
|
|
213
|
-
the command exits `0`. A gate that must also catch a failed config should check that the command
|
|
214
|
-
reported migrations at all.
|
|
215
|
-
- **Requires a `@spinajs/cli` that propagates `process.exitCode`.** Earlier versions ended the
|
|
216
|
-
bin's success path with a bare `process.exit(0)`, which discards whatever a command set — driven
|
|
217
|
-
through such a bin, `migrate-status` exits `0` even with pending work. If you are pinned to one,
|
|
218
|
-
call the command class directly (see the snippet at the top) rather than going through the bin.
|
|
219
|
-
|
|
220
|
-
## The blocking guarantee is best-effort
|
|
221
|
-
|
|
222
|
-
A failed migration blocks every later `migrate-up` on its connection. That is what makes
|
|
223
|
-
`migrate-status` + `migrate-resolve` a safe recovery loop instead of a suggestion: a half-applied
|
|
224
|
-
schema change cannot be built on top of.
|
|
225
|
-
|
|
226
|
-
The guarantee holds only as far as the bookkeeping does. When a migration fails, the ORM writes
|
|
227
|
-
the failure into the tracking table — and if *that* write fails too (the connection dropped, the
|
|
228
|
-
table is locked), the error is caught and logged rather than raised. The run still fails, but the
|
|
229
|
-
row that would have blocked the next `migrate-up` was never written, and the next run proceeds as
|
|
230
|
-
if nothing had happened.
|
|
231
|
-
|
|
232
|
-
In practice this needs the database to fail twice, in a specific order. It matters when you are
|
|
233
|
-
reading logs after an incident: a `migrate-up` that succeeded shortly after a failed one is not
|
|
234
|
-
by itself proof that the failure was resolved. Check `migrate-status`.
|
|
235
|
-
|
|
236
|
-
## Notes
|
|
237
|
-
|
|
238
|
-
- Migrations run against a schema no model is wired to yet. Use the `OrmDriver` passed to `up()`,
|
|
239
|
-
never a model class. The `data()` hook runs later, once models are available.
|
|
240
|
-
- `--fake` records the outcome without executing anything, on both `migrate-up` and
|
|
241
|
-
`migrate-down`. It is for a database that was changed out of band and needs the tracking table
|
|
242
|
-
brought in line.
|
|
243
|
-
- `migrate-status` reports every configured connection, including ones whose
|
|
244
|
-
`Migration.OnStartup` is off — hiding those would answer "nothing to see" for exactly the
|
|
245
|
-
connections somebody is most likely asking about. It has no `--connection` of its own, for the
|
|
246
|
-
same reason: the report is the deploy gate, and a gate that can be narrowed is a gate that can
|
|
247
|
-
be talked past.
|
|
1
|
+
# @spinajs/orm-cli
|
|
2
|
+
|
|
3
|
+
Command line front end for spinajs ORM migrations. Five commands — apply, roll back, report,
|
|
4
|
+
force a state, scaffold — over the `orm.Migration` facade in `@spinajs/orm`.
|
|
5
|
+
|
|
6
|
+
The package is a thin wrapper on purpose. Everything that decides what a migration run means
|
|
7
|
+
lives in `@spinajs/orm`; what lives here is the argument handling, the operator-facing wording
|
|
8
|
+
and the exit codes. The dependency runs one way only — `orm-cli` → `orm` — so the ORM stays
|
|
9
|
+
usable, and testable, with no CLI in its dependency tree.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @spinajs/orm-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package ships a config fragment that appends its own command directory to
|
|
18
|
+
`system.dirs.cli`, which is where `@spinajs/cli` looks for commands. Installing it is therefore
|
|
19
|
+
enough — `spinajs migrate-status` works with no import and no wiring on your side. If your
|
|
20
|
+
application builds its own command list instead, `import '@spinajs/orm-cli'` is all that is
|
|
21
|
+
needed: `@Command` registers each class in DI the moment the module is evaluated.
|
|
22
|
+
|
|
23
|
+
The commands are also plain DI classes, so a script can drive them without commander:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { DI } from '@spinajs/di';
|
|
27
|
+
import { MigrateStatusCommand } from '@spinajs/orm-cli';
|
|
28
|
+
|
|
29
|
+
await (await DI.resolve(MigrateStatusCommand)).execute();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Running a command never migrates anything
|
|
33
|
+
|
|
34
|
+
Every command starts by resolving an `Orm`, and an ordinary `DI.resolve(Orm)` ends with the boot
|
|
35
|
+
migration pass — every pending migration on every connection whose `Migration.OnStartup` is on.
|
|
36
|
+
For an application that is the point. For a migration tool it is a trap, twice over:
|
|
37
|
+
|
|
38
|
+
- a connection holding a **failed** migration refuses every migration run, so the resolve throws
|
|
39
|
+
before the command body starts. That took down every command on the row it was invoked about,
|
|
40
|
+
including `migrate-resolve` — the one command that clears it, and the one the refusal names as
|
|
41
|
+
the remedy.
|
|
42
|
+
- `migrate-status` would apply everything pending and only then report, so the deploy gate asking
|
|
43
|
+
"is this database current?" made it current, answered "yes" and exited `0`, with the DDL it was
|
|
44
|
+
meant to hold back already run.
|
|
45
|
+
|
|
46
|
+
So the commands resolve their Orm through `resolveCliOrm()`, which passes `MigrateOnStartup:
|
|
47
|
+
false` (an `IOrmOptions` field of `@spinajs/orm`). Everything else about resolving happens —
|
|
48
|
+
connections, models, value converters, `orm.Migration` — only the boot pass is skipped. It is
|
|
49
|
+
opt-**in**: nothing changes for an application that resolves an Orm the ordinary way, and this
|
|
50
|
+
package ships no configuration that would switch startup migrations off for anybody.
|
|
51
|
+
|
|
52
|
+
Two consequences worth knowing:
|
|
53
|
+
|
|
54
|
+
- `migrate-up --fake` means what it says on a `Migration.OnStartup` connection. A boot pass would
|
|
55
|
+
have really applied the migrations the flag promises only to record.
|
|
56
|
+
- **A migration applied by the CLI never gets its `data()` hook.** Seeding belongs to the boot
|
|
57
|
+
pass: `Orm.resolve()` seeds what its own startup run applied, and a later boot finds the
|
|
58
|
+
migration already applied and seeds nothing. That was already true of every connection with
|
|
59
|
+
`Migration.OnStartup` off; it is now true of all of them. Migrations that must be seeded have to
|
|
60
|
+
be applied by an application boot, not by `migrate-up`.
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
| Command | Options | Does |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `migrate-up` | `-n, --name [name]`, `-c, --connection [connection]`, `-f, --fake` | Applies pending migrations on every configured connection |
|
|
67
|
+
| `migrate-down` | `-n, --name [name]`, `-c, --connection [connection]`, `-a, --all`, `-f, --fake` | Rolls back — **the last applied batch only** unless `--all` |
|
|
68
|
+
| `migrate-status` | — | Prints one line per migration per connection; the deploy gate |
|
|
69
|
+
| `migrate-resolve` | `-n, --name [name]` (required), `--applied`, `--rolled-back` | Records the outcome of a FAILED migration |
|
|
70
|
+
| `migrate-create` | `-n, --name [name]` (required), `-d, --dir [dir]`, `-c, --connection [connection]` | Scaffolds a migration file |
|
|
71
|
+
|
|
72
|
+
### `migrate-up`
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
spinajs migrate-up
|
|
76
|
+
spinajs migrate-up --name AddUserTable_2026_07_29_10_00_00
|
|
77
|
+
spinajs migrate-up --connection reporting # this connection only
|
|
78
|
+
spinajs migrate-up --fake # record as applied without running anything
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Without `--name` it applies everything pending, in `(timestamp, name)` order, across every
|
|
82
|
+
configured connection. With `--name` it applies exactly that one.
|
|
83
|
+
|
|
84
|
+
`--connection` limits the run to one connection. Every other configured connection is left
|
|
85
|
+
completely untouched — its migration service is never reached, so its tracking table is not even
|
|
86
|
+
created. The name is matched against the configured connections (aliases included, since they
|
|
87
|
+
resolve to the same connection), and one nothing answers to **throws** rather than running
|
|
88
|
+
nothing: a filter that silently matched nothing would exit `0` reporting "0 migrations applied".
|
|
89
|
+
|
|
90
|
+
Two named-run outcomes are deliberately **not** reported as success:
|
|
91
|
+
|
|
92
|
+
- the name matches nothing in the registry — the facade throws rather than returning an empty
|
|
93
|
+
list, because "0 migrations applied" from a typo is indistinguishable from "already current";
|
|
94
|
+
- the name is registered but the connection it declares is not configured in this deployment.
|
|
95
|
+
The facade only warns and returns `[]` there, so this command checks `status()` afterwards and
|
|
96
|
+
exits non-zero with an explanation.
|
|
97
|
+
|
|
98
|
+
### `migrate-down`
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
spinajs migrate-down # the LAST APPLIED BATCH, not everything
|
|
102
|
+
spinajs migrate-down --all # every applied migration, on every connection
|
|
103
|
+
spinajs migrate-down --name AddUserTable_2026_07_29_10_00_00
|
|
104
|
+
spinajs migrate-down --connection reporting --all # everything, on one connection
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The default scope is the last applied batch — one `migrate-up` run undone, not the whole
|
|
108
|
+
history. `--all` reverses everything. `--connection` narrows whichever of those two applies, and
|
|
109
|
+
is announced first for that reason: `--all --connection reporting` is "every applied migration on
|
|
110
|
+
*one* connection". The command says which scope it is about to reverse *before* it does it,
|
|
111
|
+
because by the time the result line prints, the schema has already changed.
|
|
112
|
+
|
|
113
|
+
A rollback drops the tracking row rather than stamping it "rolled back": the table is meant to
|
|
114
|
+
hold only migrations that are actually present in the database, and both a missing row and a
|
|
115
|
+
rolled-back one read as pending to the next `migrate-up`.
|
|
116
|
+
|
|
117
|
+
`--name` has a known sharp edge in the migration service: it is handed a one-element unit list,
|
|
118
|
+
so every *other* applied row in the target batch looks unmatched and gets warned about as
|
|
119
|
+
"no registered migration matches them (file deleted or renamed)". Those rows are healthy, and
|
|
120
|
+
the remedy that warning suggests — removing the row by hand — is destructive here. This command
|
|
121
|
+
prints a line saying exactly that before the run, so the warnings can be ignored.
|
|
122
|
+
|
|
123
|
+
### `migrate-status`
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
spinajs migrate-status
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
STATE BATCH CONNECTION MIGRATION
|
|
131
|
+
applied 1 default AddUserTable_2026_07_29_10_00_00
|
|
132
|
+
!! FAILED 0 default AddOrderIndex_2026_07_29_11_00_00
|
|
133
|
+
?? INTERRUPTED 0 default BackfillTotals_2026_07_29_12_00_00
|
|
134
|
+
pending - default AddInvoices_2026_07_30_09_00_00
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Output goes to stdout via `console.log`, not through the framework logger: it is this command's
|
|
138
|
+
*product*, something an operator greps and a script pipes, and routing it through the log would
|
|
139
|
+
let a configured level or target swallow it.
|
|
140
|
+
|
|
141
|
+
A failed row carries `!!` in the leftmost column, not just the word `FAILED`. That row is the
|
|
142
|
+
one line in the report that stops every later `migrate-up` on its connection, and it has to
|
|
143
|
+
survive being skimmed in a wall of `applied`. Below the table the command prints the two exact
|
|
144
|
+
`migrate-resolve` invocations for each failed migration.
|
|
145
|
+
|
|
146
|
+
`??` marks an **interrupted** migration — one that was started and never finished, because the
|
|
147
|
+
process running it was killed before it could record either outcome. It carries the opposite
|
|
148
|
+
warning to `FAILED`: it blocks nothing, and the next `migrate-up` re-runs it from the top, whether
|
|
149
|
+
or not anybody looked. Under the default `Transaction.Mode: None` that means non-idempotent data
|
|
150
|
+
changes get applied twice, silently. The same two `migrate-resolve` invocations are printed for
|
|
151
|
+
it. See "Interrupted runs" in
|
|
152
|
+
[the ORM migration docs](../orm/docs/10-schema-and-migrations.md#interrupted-runs).
|
|
153
|
+
|
|
154
|
+
`[checksum mismatch]` marks a migration whose source changed after it was applied. It is
|
|
155
|
+
reported but does **not** on its own make the command exit non-zero — only pending and failed
|
|
156
|
+
work do.
|
|
157
|
+
|
|
158
|
+
### `migrate-resolve`
|
|
159
|
+
|
|
160
|
+
The escape hatch for a run that died halfway. Valid on the two row shapes whose real outcome
|
|
161
|
+
nobody recorded — **failed** (`FinishedAt` NULL and `Logs` set) and **interrupted** (`StartedAt`
|
|
162
|
+
set, `FinishedAt` and `Logs` both NULL). Anything healthy, rolled back or absent is refused rather
|
|
163
|
+
than silently rewritten.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --applied # the change IS in the database
|
|
167
|
+
spinajs migrate-resolve --name AddOrderIndex_2026_07_29_11_00_00 --rolled-back # the change is NOT
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Exactly one of the two flags, never both and never neither: the point of the command is to state
|
|
171
|
+
which of the two things actually happened, and neither the CLI nor the ORM can find that out on
|
|
172
|
+
its own. The refusal happens before any Orm is resolved, so a malformed command line never opens
|
|
173
|
+
a database connection.
|
|
174
|
+
|
|
175
|
+
`--rolled-back` makes the migration pending again — it *will* run on the next `migrate-up`.
|
|
176
|
+
|
|
177
|
+
### `migrate-create`
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
spinajs migrate-create --name AddInvoices
|
|
181
|
+
spinajs migrate-create --name AddInvoices --dir ./src/migrations --connection reporting
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Prints the path it wrote, on its own line, so `$(spinajs migrate-create -n AddInvoices)` is
|
|
185
|
+
usable. Defaults: `./src/migrations` and the `default` connection.
|
|
186
|
+
|
|
187
|
+
`--name` takes the *prefix* only, letters and digits, starting with a letter. The
|
|
188
|
+
`_yyyy_MM_dd_HH_mm_ss` suffix is appended here, and it is not decoration: that timestamp is the
|
|
189
|
+
only ordering the migration runner has, and it is read back out of the class name. A name the
|
|
190
|
+
runner cannot parse is refused up front, and an existing file is never overwritten.
|
|
191
|
+
|
|
192
|
+
The generated class only takes effect once it is *imported* — the `@Migration` decorator has to
|
|
193
|
+
run to register it. Re-export it from your package or application index, the way `src/migrations/*.ts`
|
|
194
|
+
files are re-exported elsewhere in spinajs.
|
|
195
|
+
|
|
196
|
+
## Exit codes
|
|
197
|
+
|
|
198
|
+
| Command | `0` | non-zero |
|
|
199
|
+
| --- | --- | --- |
|
|
200
|
+
| `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 |
|
|
201
|
+
| `migrate-down` | rollback completed, or nothing to roll back | a `--connection` nothing answers to; any error from the run |
|
|
202
|
+
| `migrate-status` | every migration is applied | anything is pending or failed |
|
|
203
|
+
| `migrate-resolve` | the state was recorded | both/neither flag given; the row is neither failed nor interrupted |
|
|
204
|
+
| `migrate-create` | file written | invalid name or connection; the file already exists |
|
|
205
|
+
|
|
206
|
+
`migrate-status` is meant to be a deploy gate — "is this database current?" — so an un-run
|
|
207
|
+
migration is a "no", not just a failed one.
|
|
208
|
+
|
|
209
|
+
Two things the table does not say:
|
|
210
|
+
|
|
211
|
+
- **A `0` from `migrate-status` means "nothing is pending", not "the database is reachable and
|
|
212
|
+
configured".** With no connections configured, nothing is registered, so nothing is pending and
|
|
213
|
+
the command exits `0`. A gate that must also catch a failed config should check that the command
|
|
214
|
+
reported migrations at all.
|
|
215
|
+
- **Requires a `@spinajs/cli` that propagates `process.exitCode`.** Earlier versions ended the
|
|
216
|
+
bin's success path with a bare `process.exit(0)`, which discards whatever a command set — driven
|
|
217
|
+
through such a bin, `migrate-status` exits `0` even with pending work. If you are pinned to one,
|
|
218
|
+
call the command class directly (see the snippet at the top) rather than going through the bin.
|
|
219
|
+
|
|
220
|
+
## The blocking guarantee is best-effort
|
|
221
|
+
|
|
222
|
+
A failed migration blocks every later `migrate-up` on its connection. That is what makes
|
|
223
|
+
`migrate-status` + `migrate-resolve` a safe recovery loop instead of a suggestion: a half-applied
|
|
224
|
+
schema change cannot be built on top of.
|
|
225
|
+
|
|
226
|
+
The guarantee holds only as far as the bookkeeping does. When a migration fails, the ORM writes
|
|
227
|
+
the failure into the tracking table — and if *that* write fails too (the connection dropped, the
|
|
228
|
+
table is locked), the error is caught and logged rather than raised. The run still fails, but the
|
|
229
|
+
row that would have blocked the next `migrate-up` was never written, and the next run proceeds as
|
|
230
|
+
if nothing had happened.
|
|
231
|
+
|
|
232
|
+
In practice this needs the database to fail twice, in a specific order. It matters when you are
|
|
233
|
+
reading logs after an incident: a `migrate-up` that succeeded shortly after a failed one is not
|
|
234
|
+
by itself proof that the failure was resolved. Check `migrate-status`.
|
|
235
|
+
|
|
236
|
+
## Notes
|
|
237
|
+
|
|
238
|
+
- Migrations run against a schema no model is wired to yet. Use the `OrmDriver` passed to `up()`,
|
|
239
|
+
never a model class. The `data()` hook runs later, once models are available.
|
|
240
|
+
- `--fake` records the outcome without executing anything, on both `migrate-up` and
|
|
241
|
+
`migrate-down`. It is for a database that was changed out of band and needs the tracking table
|
|
242
|
+
brought in line.
|
|
243
|
+
- `migrate-status` reports every configured connection, including ones whose
|
|
244
|
+
`Migration.OnStartup` is off — hiding those would answer "nothing to see" for exactly the
|
|
245
|
+
connections somebody is most likely asking about. It has no `--connection` of its own, for the
|
|
246
|
+
same reason: the report is the deploy gate, and a gate that can be narrowed is a gate that can
|
|
247
|
+
be talked past.
|
|
@@ -112,30 +112,30 @@ exports.DEFAULT_MIGRATION_CONNECTION = 'default';
|
|
|
112
112
|
* repo's own convention for a migration whose `down()` legitimately ignores it.
|
|
113
113
|
*/
|
|
114
114
|
function migrationTemplate(cls, connection, env) {
|
|
115
|
-
return `/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
116
|
-
import { Migration, OrmDriver, OrmMigration } from '@spinajs/orm';
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* TODO: describe the schema change this migration makes.
|
|
120
|
-
*/
|
|
121
|
-
@Migration('${connection}'${env ? `, { Env: '${env}' }` : ''})
|
|
122
|
-
export class ${cls} extends OrmMigration {
|
|
123
|
-
/**
|
|
124
|
-
* Schema changes. Models are NOT wired up yet at this point - reach the database through
|
|
125
|
-
* \`connection\`, never through a model class.
|
|
126
|
-
*/
|
|
127
|
-
public async up(connection: OrmDriver): Promise<void> {
|
|
128
|
-
// TODO: await connection.schema().createTable('table_name', (table) => { ... });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Undoes \`up()\`. Leave it empty only when the change genuinely cannot be reversed - an empty
|
|
133
|
-
* \`down()\` makes migrate-down report success while changing nothing.
|
|
134
|
-
*/
|
|
135
|
-
public async down(connection: OrmDriver): Promise<void> {
|
|
136
|
-
// TODO: await connection.schema().dropTable('table_name');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
115
|
+
return `/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
116
|
+
import { Migration, OrmDriver, OrmMigration } from '@spinajs/orm';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* TODO: describe the schema change this migration makes.
|
|
120
|
+
*/
|
|
121
|
+
@Migration('${connection}'${env ? `, { Env: '${env}' }` : ''})
|
|
122
|
+
export class ${cls} extends OrmMigration {
|
|
123
|
+
/**
|
|
124
|
+
* Schema changes. Models are NOT wired up yet at this point - reach the database through
|
|
125
|
+
* \`connection\`, never through a model class.
|
|
126
|
+
*/
|
|
127
|
+
public async up(connection: OrmDriver): Promise<void> {
|
|
128
|
+
// TODO: await connection.schema().createTable('table_name', (table) => { ... });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Undoes \`up()\`. Leave it empty only when the change genuinely cannot be reversed - an empty
|
|
133
|
+
* \`down()\` makes migrate-down report success while changing nothing.
|
|
134
|
+
*/
|
|
135
|
+
public async down(connection: OrmDriver): Promise<void> {
|
|
136
|
+
// TODO: await connection.schema().dropTable('table_name');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
139
|
`;
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
@@ -74,30 +74,30 @@ export const DEFAULT_MIGRATION_CONNECTION = 'default';
|
|
|
74
74
|
* repo's own convention for a migration whose `down()` legitimately ignores it.
|
|
75
75
|
*/
|
|
76
76
|
export function migrationTemplate(cls, connection, env) {
|
|
77
|
-
return `/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
78
|
-
import { Migration, OrmDriver, OrmMigration } from '@spinajs/orm';
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* TODO: describe the schema change this migration makes.
|
|
82
|
-
*/
|
|
83
|
-
@Migration('${connection}'${env ? `, { Env: '${env}' }` : ''})
|
|
84
|
-
export class ${cls} extends OrmMigration {
|
|
85
|
-
/**
|
|
86
|
-
* Schema changes. Models are NOT wired up yet at this point - reach the database through
|
|
87
|
-
* \`connection\`, never through a model class.
|
|
88
|
-
*/
|
|
89
|
-
public async up(connection: OrmDriver): Promise<void> {
|
|
90
|
-
// TODO: await connection.schema().createTable('table_name', (table) => { ... });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Undoes \`up()\`. Leave it empty only when the change genuinely cannot be reversed - an empty
|
|
95
|
-
* \`down()\` makes migrate-down report success while changing nothing.
|
|
96
|
-
*/
|
|
97
|
-
public async down(connection: OrmDriver): Promise<void> {
|
|
98
|
-
// TODO: await connection.schema().dropTable('table_name');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
77
|
+
return `/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
78
|
+
import { Migration, OrmDriver, OrmMigration } from '@spinajs/orm';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* TODO: describe the schema change this migration makes.
|
|
82
|
+
*/
|
|
83
|
+
@Migration('${connection}'${env ? `, { Env: '${env}' }` : ''})
|
|
84
|
+
export class ${cls} extends OrmMigration {
|
|
85
|
+
/**
|
|
86
|
+
* Schema changes. Models are NOT wired up yet at this point - reach the database through
|
|
87
|
+
* \`connection\`, never through a model class.
|
|
88
|
+
*/
|
|
89
|
+
public async up(connection: OrmDriver): Promise<void> {
|
|
90
|
+
// TODO: await connection.schema().createTable('table_name', (table) => { ... });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Undoes \`up()\`. Leave it empty only when the change genuinely cannot be reversed - an empty
|
|
95
|
+
* \`down()\` makes migrate-down report success while changing nothing.
|
|
96
|
+
*/
|
|
97
|
+
public async down(connection: OrmDriver): Promise<void> {
|
|
98
|
+
// TODO: await connection.schema().dropTable('table_name');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
101
|
`;
|
|
102
102
|
}
|
|
103
103
|
/**
|