@spinajs/orm-cli 2.0.491 → 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 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
  /**