@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8
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/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
- package/dist/chunk-SPQUX3C7.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
package/docs/console.md
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# Oriel console
|
|
2
|
+
|
|
3
|
+
`oriel` is your app's command line. It runs the framework's commands, such as `migrate` and `make:model`, and the
|
|
4
|
+
commands you write for your own app.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
`oriel` is a script at the root of your app. Run it with Node, or with Bun. With no command, or with `list`, it lists
|
|
9
|
+
every command it knows:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
node oriel list
|
|
13
|
+
bun oriel list
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`list` takes a group's name, the part of a command's name before its `:`, to list only that group:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
node oriel list make
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every command has a help screen, with its arguments and options. Give the command's name to `help`, or add `--help`
|
|
23
|
+
(or `-h`) to the command:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
node oriel help migrate
|
|
27
|
+
node oriel migrate --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A name `oriel` doesn't know is an error, with the names closest to it: "Command "migrat" is not defined.", then "Did
|
|
31
|
+
you mean one of these?" and up to three names.
|
|
32
|
+
|
|
33
|
+
On Node, the script loads your TypeScript with `tsx`, which a new app has among its dependencies; Bun runs it as it
|
|
34
|
+
is. Either way, it creates your app from `bootstrap/app.ts`, boots it, runs the command, and shuts the app down,
|
|
35
|
+
closing its database connections and writing out its logs, before it exits with the command's exit code.
|
|
36
|
+
|
|
37
|
+
### Tinker (REPL)
|
|
38
|
+
|
|
39
|
+
`tinker` opens Node's REPL with your app in it, as `app`:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
node oriel tinker
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Type JavaScript at the `oriel>` prompt, `await` included. `.exit` (or Ctrl+D) leaves it.
|
|
46
|
+
|
|
47
|
+
> [!NOTE]
|
|
48
|
+
> Laravel's Tinker is PsySH, with your models and facades at hand and a config file of its own. Oriel's `tinker` is
|
|
49
|
+
> Node's own REPL, with `app` in its context and nothing else, and it has no settings.
|
|
50
|
+
|
|
51
|
+
## Available commands
|
|
52
|
+
|
|
53
|
+
These are the commands every app has. A new app adds one of its own, `inspire`, in `routes/console.ts`.
|
|
54
|
+
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `about` | Shows your app's name, the Oriel and Node versions, the environment, debug mode and URL |
|
|
58
|
+
| `help` | Shows a command's help screen |
|
|
59
|
+
| `list` | Lists the commands, all of them or one group's |
|
|
60
|
+
| `serve` | Serves your app on Oriel's server; `--watch` restarts it when your code changes |
|
|
61
|
+
| `test` | Runs your tests with Vitest |
|
|
62
|
+
| `tinker` | Opens Node's REPL with your app in it |
|
|
63
|
+
| `db:seed` | Seeds the database, with `DatabaseSeeder` or the seeder you name |
|
|
64
|
+
| `event:list` | Lists your events and their listeners |
|
|
65
|
+
| `inertia:check-ssr` | Checks that the Inertia SSR server is up |
|
|
66
|
+
| `inertia:middleware` | Writes an Inertia middleware class, `HandleInertiaRequests` unless you name another |
|
|
67
|
+
| `inertia:start-ssr` | Starts the Inertia SSR server |
|
|
68
|
+
| `inertia:stop-ssr` | Stops the Inertia SSR server |
|
|
69
|
+
| `inertia:types` | Writes the TypeScript types of your Inertia pages' props |
|
|
70
|
+
| `install:api` | Writes `routes/api.ts` and adds it to `bootstrap/app.ts` |
|
|
71
|
+
| `key:generate` | Sets `APP_KEY` in `.env`; `--show` prints a key instead |
|
|
72
|
+
| `make:cast` | Writes a custom cast class in `app/Casts` |
|
|
73
|
+
| `make:command` | Writes a console command class in `app/Console/Commands` |
|
|
74
|
+
| `make:controller` | Writes a controller in `app/Http/Controllers` |
|
|
75
|
+
| `make:event` | Writes an event class in `app/Events` |
|
|
76
|
+
| `make:factory` | Writes a model factory in `database/factories` |
|
|
77
|
+
| `make:job` | Writes a queued job in `app/Jobs`; `--sync` writes one that runs at once |
|
|
78
|
+
| `make:listener` | Writes an event listener in `app/Listeners`; `--event` names its event, `--queued` queues it |
|
|
79
|
+
| `make:live` | Writes a Live component; `--test` writes its test too |
|
|
80
|
+
| `make:mail` | Writes a mailable in `app/Mail`, and its view |
|
|
81
|
+
| `make:middleware` | Writes a middleware class in `app/Http/Middleware` |
|
|
82
|
+
| `make:migration` | Writes a migration in `database/migrations`; `--create` or `--table` names its table |
|
|
83
|
+
| `make:model` | Writes a model in `app/Models`, and the files its options ask for |
|
|
84
|
+
| `make:notification` | Writes a notification in `app/Notifications` |
|
|
85
|
+
| `make:observer` | Writes a model observer in `app/Observers` |
|
|
86
|
+
| `make:policy` | Writes a policy in `app/Policies`; `--model` names its model |
|
|
87
|
+
| `make:provider` | Writes a service provider in `app/Providers` |
|
|
88
|
+
| `make:request` | Writes a form request in `app/Http/Requests` |
|
|
89
|
+
| `make:resource` | Writes a resource class, whose `toArray()` shapes a model for JSON, in `app/Http/Resources`; `--collection` writes one for a list |
|
|
90
|
+
| `make:rule` | Writes a validation rule object in `app/Rules` |
|
|
91
|
+
| `make:seeder` | Writes a seeder in `database/seeders` |
|
|
92
|
+
| `make:test` | Writes a test in `tests/Feature`, or in `tests/Unit` with `--unit` |
|
|
93
|
+
| `migrate` | Runs the migrations that haven't run yet |
|
|
94
|
+
| `migrate:fresh` | Drops every table and runs every migration again |
|
|
95
|
+
| `migrate:install` | Creates the table that records which migrations have run |
|
|
96
|
+
| `migrate:refresh` | Rolls back every migration and runs them all again |
|
|
97
|
+
| `migrate:reset` | Rolls back every migration |
|
|
98
|
+
| `migrate:rollback` | Rolls back the last batch of migrations |
|
|
99
|
+
| `migrate:status` | Shows whether each migration has run |
|
|
100
|
+
| `model:types` | Writes the TypeScript types of your models' attributes to `types/models.d.ts` |
|
|
101
|
+
| `queue:clear` | Deletes the jobs waiting on a queue |
|
|
102
|
+
| `queue:failed` | Lists the failed jobs |
|
|
103
|
+
| `queue:flush` | Deletes every failed job |
|
|
104
|
+
| `queue:forget` | Deletes one failed job |
|
|
105
|
+
| `queue:listen` | Runs the queue's jobs, each in a fresh process |
|
|
106
|
+
| `queue:restart` | Tells every queue worker to stop after its current job |
|
|
107
|
+
| `queue:retry` | Puts failed jobs back on their queue |
|
|
108
|
+
| `queue:work` | Runs the queue's jobs until you stop it |
|
|
109
|
+
| `route:list` | Lists your routes |
|
|
110
|
+
| `route:types` | Writes the TypeScript types of your route names and parameters, and the route table for your pages |
|
|
111
|
+
| `schedule:clear-cache` | Releases the locks that `withoutOverlapping()` holds |
|
|
112
|
+
| `schedule:interrupt` | Stops a running `schedule:run` from repeating its every-few-seconds tasks |
|
|
113
|
+
| `schedule:list` | Lists your scheduled tasks, with their cron expressions |
|
|
114
|
+
| `schedule:run` | Runs the scheduled tasks that are due |
|
|
115
|
+
| `schedule:work` | Runs the tasks that are due, every minute, until you stop it |
|
|
116
|
+
| `storage:link` | Links `public/storage` to `storage/app/public` |
|
|
117
|
+
|
|
118
|
+
`node oriel help <command>` lists a command's options. A few that are worth knowing up front:
|
|
119
|
+
|
|
120
|
+
- `make:model Flight -mcrfsa --policy` writes the model with every file it can have: its migration (`-m`), a resource
|
|
121
|
+
controller typed with the model (`-c`, `-r`), its factory (`-f`), its seeder (`-s`) and its policy (`--policy`).
|
|
122
|
+
`-a` (`--all`) asks for all of them, and for the `StoreFlightRequest` and `UpdateFlightRequest` form requests the
|
|
123
|
+
controller validates with. Short flags combine, as here.
|
|
124
|
+
- `make:controller` takes `--model=Photo`, `--invokable`, `--resource`, `--api` and `--requests`:
|
|
125
|
+
[Generating controllers](controllers.md#generating-controllers) shows what each one writes. A model whose name is a
|
|
126
|
+
reserved word gets a parameter with a `Model` suffix: `--model=Case` writes `show(caseModel: Case)`.
|
|
127
|
+
- `make:mail OrderShipped` writes the mailable and its view, `resources/views/mail/order-shipped.tsx`, which the
|
|
128
|
+
mailable renders as `mail.order-shipped`. `--view` names another view. A view that exists is left as it is, even
|
|
129
|
+
with `--force`.
|
|
130
|
+
- `serve --watch` restarts the server when a file it runs changes. [Running your
|
|
131
|
+
application](installation.md#running-your-application) says which files those are.
|
|
132
|
+
- A `make:` command doesn't replace a file that exists, unless you pass `--force`. `make:migration` writes a new file
|
|
133
|
+
every time, its name starting with the date and time.
|
|
134
|
+
|
|
135
|
+
The `schedule:` commands run the tasks you schedule in `routes/console.ts`. Your server's cron calls
|
|
136
|
+
`node oriel schedule:run` every minute; while you work, `node oriel schedule:work` does it for you.
|
|
137
|
+
[Task scheduling](scheduling.md) covers them.
|
|
138
|
+
|
|
139
|
+
## Writing commands
|
|
140
|
+
|
|
141
|
+
Besides the framework's commands, you can write your own. They live in `app/Console/Commands`, where `oriel` finds
|
|
142
|
+
them by itself.
|
|
143
|
+
|
|
144
|
+
### Generating commands
|
|
145
|
+
|
|
146
|
+
`make:command` writes a new command class in `app/Console/Commands`, and creates the folder if your app doesn't have
|
|
147
|
+
it yet:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
node oriel make:command SendEmails
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The class is named as you give it. Its signature is `app:` and the class's name in kebab case, here `app:send-emails`,
|
|
154
|
+
unless you pass another with `--command`:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
node oriel make:command SendEmails --command=mail:send
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Command structure
|
|
161
|
+
|
|
162
|
+
Once the command is written, set its `signature` and its `description`, which `list` and `help` show. The signature
|
|
163
|
+
is the command's name and the [input it expects](#defining-input-expectations). `handle()` runs when the command is
|
|
164
|
+
called.
|
|
165
|
+
|
|
166
|
+
Here is a command that sends a marketing email to a user. Its work is done by a service, which `handle()` takes from
|
|
167
|
+
the container with `inject()`:
|
|
168
|
+
|
|
169
|
+
```ts title="app/Console/Commands/SendEmails.ts"
|
|
170
|
+
// @filename: app/Support/DripEmailer.ts
|
|
171
|
+
import type { User } from '../Models/User'
|
|
172
|
+
export declare class DripEmailer {
|
|
173
|
+
send(user: User): Promise<void>
|
|
174
|
+
}
|
|
175
|
+
// ---cut---
|
|
176
|
+
import { Command, inject } from '@orieljs/framework'
|
|
177
|
+
import { User } from '../../Models/User'
|
|
178
|
+
import { DripEmailer } from '../../Support/DripEmailer'
|
|
179
|
+
|
|
180
|
+
export class SendEmails extends Command {
|
|
181
|
+
readonly signature = 'mail:send {user}'
|
|
182
|
+
override description = 'Send a marketing email to a user'
|
|
183
|
+
|
|
184
|
+
async handle(): Promise<void> {
|
|
185
|
+
const user = await User.findOrFail(this.argument('user'))
|
|
186
|
+
|
|
187
|
+
await inject(DripEmailer).send(user)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Each run of a command has its own scope, as a request does, so `inject()`, the helpers and the facades work inside
|
|
193
|
+
`handle()`, and a scoped service lasts for that one run.
|
|
194
|
+
|
|
195
|
+
> [!NOTE]
|
|
196
|
+
> Laravel injects the services that `handle()` type-hints. TypeScript types are gone when your app runs, so Oriel
|
|
197
|
+
> calls `handle()` with no arguments: take a service with `inject()` or its facade, inside `handle()`.
|
|
198
|
+
|
|
199
|
+
#### Exit codes
|
|
200
|
+
|
|
201
|
+
A command that returns nothing exits with `0`, which means success. Return a number to exit with another code:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
export class SendEmails extends Command {
|
|
205
|
+
readonly signature = 'mail:send {user}'
|
|
206
|
+
|
|
207
|
+
handle(): number {
|
|
208
|
+
this.error('Something went wrong.')
|
|
209
|
+
|
|
210
|
+
return 1
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A command that throws exits with `1`, and `oriel` prints the error with its stack.
|
|
216
|
+
|
|
217
|
+
> [!NOTE]
|
|
218
|
+
> Laravel's `$this->fail()` isn't in Oriel. Print the message with `this.error()` and return `1`, or throw.
|
|
219
|
+
|
|
220
|
+
### Closure commands
|
|
221
|
+
|
|
222
|
+
A closure command is to a command class what a route's closure is to a controller. Define them in
|
|
223
|
+
`routes/console.ts` with `Artisan.command()`, which takes the signature and a function:
|
|
224
|
+
|
|
225
|
+
```ts title="routes/console.ts"
|
|
226
|
+
import { Artisan } from '@orieljs/framework'
|
|
227
|
+
import type { CommandContext } from '@orieljs/framework/contracts'
|
|
228
|
+
|
|
229
|
+
Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {
|
|
230
|
+
this.info(`Sending email to: ${String(user)}!`)
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The function receives the command's arguments, in the order the signature lists them, and its `this` is the command,
|
|
235
|
+
with a command class's methods for its input and output: `this.option()`, `this.info()`, `this.ask()` and the rest.
|
|
236
|
+
Write a `function`, not an arrow function, so that `this` is the command. The arguments are typed `unknown`: an argument
|
|
237
|
+
is a string, or a list of strings for an [array argument](#input-arrays).
|
|
238
|
+
|
|
239
|
+
> [!NOTE]
|
|
240
|
+
> Laravel matches a closure's parameters to the arguments and options by name, and injects any service it
|
|
241
|
+
> type-hints. Oriel passes the arguments only, by position: read an option with `this.option()`, and a service with
|
|
242
|
+
> `inject()`.
|
|
243
|
+
|
|
244
|
+
#### Closure command descriptions
|
|
245
|
+
|
|
246
|
+
`purpose()` gives a closure command its description, which `list` and `help` show:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
Artisan.command('mail:send {user}', function (this: CommandContext, user: unknown) {
|
|
250
|
+
// ...
|
|
251
|
+
}).purpose('Send a marketing email to a user')
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Isolatable commands
|
|
255
|
+
|
|
256
|
+
Laravel's `Isolatable` commands aren't in Oriel. To keep a command from running twice at the same time, take a
|
|
257
|
+
[cache lock](cache.md#atomic-locks) in its `handle()`, and return when another run holds it.
|
|
258
|
+
|
|
259
|
+
## Defining input expectations
|
|
260
|
+
|
|
261
|
+
The signature says what your command takes from the command line: its name, then its arguments and options, each in
|
|
262
|
+
braces.
|
|
263
|
+
|
|
264
|
+
### Arguments
|
|
265
|
+
|
|
266
|
+
This command takes one required argument, `user`:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
export class SendEmails extends Command {
|
|
270
|
+
readonly signature = 'mail:send {user}'
|
|
271
|
+
|
|
272
|
+
handle(): void {
|
|
273
|
+
// ...
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
A `?` makes an argument optional, and `=` gives it a default:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
mail:send {user?}
|
|
282
|
+
mail:send {user=foo}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Run without a required argument, the command stops before `handle()`, with "Not enough arguments (missing:
|
|
286
|
+
"user")." and exit code `1`.
|
|
287
|
+
|
|
288
|
+
### Options
|
|
289
|
+
|
|
290
|
+
An option is passed with two hyphens. One without a value is a switch: it is `true` when it is passed and `false`
|
|
291
|
+
when it isn't.
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
mail:send {user} {--queue}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
node oriel mail:send 1 --queue
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Options with values
|
|
302
|
+
|
|
303
|
+
An `=` after the name makes an option take a value. When it isn't passed, it is `undefined`:
|
|
304
|
+
|
|
305
|
+
```text
|
|
306
|
+
mail:send {user} {--queue=}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
node oriel mail:send 1 --queue=default
|
|
311
|
+
node oriel mail:send 1 --queue default
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A value after the `=` is the default, used when the option isn't passed:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
mail:send {user} {--queue=default}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Option shortcuts
|
|
321
|
+
|
|
322
|
+
A letter and a `|` before the name give an option a shortcut, passed with one hyphen. The value follows the letter,
|
|
323
|
+
with no `=`:
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
mail:send {user} {--Q|queue=}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```sh
|
|
330
|
+
node oriel mail:send 1 -Qdefault
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Switch shortcuts combine: `-mfs` is `-m -f -s`.
|
|
334
|
+
|
|
335
|
+
An option the signature doesn't declare is an error: "The "--bogus" option does not exist.".
|
|
336
|
+
|
|
337
|
+
### Input arrays
|
|
338
|
+
|
|
339
|
+
A `*` makes an argument take every value that follows it, as a list, so put it last:
|
|
340
|
+
|
|
341
|
+
```text
|
|
342
|
+
mail:send {user*}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
node oriel mail:send 1 2
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Here `user` is `['1', '2']`. With `?*`, the list may be empty:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
mail:send {user?*}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### Option arrays
|
|
356
|
+
|
|
357
|
+
An option that takes several values is declared with `=*`. Pass it once for each value:
|
|
358
|
+
|
|
359
|
+
```text
|
|
360
|
+
mail:send {--id=*}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
node oriel mail:send --id=1 --id=2
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
When it isn't passed, it is an empty list.
|
|
368
|
+
|
|
369
|
+
### Input descriptions
|
|
370
|
+
|
|
371
|
+
Describe an argument or an option after a colon, with a space on each side. `help` shows the descriptions. A
|
|
372
|
+
signature can run over several lines:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
export class SendEmails extends Command {
|
|
376
|
+
readonly signature = `mail:send
|
|
377
|
+
{user : The ID of the user}
|
|
378
|
+
{--queue : Whether the job should be queued}`
|
|
379
|
+
|
|
380
|
+
handle(): void {
|
|
381
|
+
// ...
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
> [!NOTE]
|
|
387
|
+
> Laravel can prompt for a missing required argument (`PromptsForMissingInput`). Oriel stops with an error instead:
|
|
388
|
+
> ask for it with [`ask()`](#prompting-for-input) when an optional argument is missing.
|
|
389
|
+
|
|
390
|
+
## Command I/O
|
|
391
|
+
|
|
392
|
+
### Retrieving input
|
|
393
|
+
|
|
394
|
+
`argument()` and `option()` read the command's input. `arguments()` and `options()` return all of them:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
declare const command: Command
|
|
398
|
+
// ---cut---
|
|
399
|
+
const userId = command.argument('user')
|
|
400
|
+
|
|
401
|
+
const all = command.arguments()
|
|
402
|
+
|
|
403
|
+
const queueName = command.option('queue')
|
|
404
|
+
|
|
405
|
+
const options = command.options()
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Arguments and option values are strings, as they arrive from the command line: `--tries=3` is `'3'`, so convert a
|
|
409
|
+
number with `Number()`. An argument is a string, a list for an array argument, or `undefined` when an optional one is
|
|
410
|
+
missing. An option is `true` or `false` for a switch, a string or `undefined` for an option with a value, and a list
|
|
411
|
+
for an option array. An argument or an option that the signature doesn't declare is `undefined`.
|
|
412
|
+
|
|
413
|
+
### Prompting for input
|
|
414
|
+
|
|
415
|
+
`ask()` asks a question and returns the answer. A second argument is the default, shown in parentheses and returned
|
|
416
|
+
when the answer is empty:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
declare const command: Command
|
|
420
|
+
// ---cut---
|
|
421
|
+
const name = await command.ask('What is your name?')
|
|
422
|
+
|
|
423
|
+
const nameOrDefault = await command.ask('What is your name?', 'Taylor')
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`secret()` asks for a password or another secret:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
declare const command: Command
|
|
430
|
+
// ---cut---
|
|
431
|
+
const password = await command.secret('What is the password?')
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
> [!WARNING]
|
|
435
|
+
> Laravel hides what the user types for `secret()`. Oriel's `secret()` reads the answer as `ask()` does, and the
|
|
436
|
+
> terminal shows it as it is typed.
|
|
437
|
+
|
|
438
|
+
#### Asking for confirmation
|
|
439
|
+
|
|
440
|
+
`confirm()` asks a yes or no question. It returns `true` when the answer is `y` or `yes`, in any case, and `false`
|
|
441
|
+
for any other answer:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
declare const command: Command
|
|
445
|
+
// ---cut---
|
|
446
|
+
if (await command.confirm('Do you wish to continue?')) {
|
|
447
|
+
// ...
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
An empty answer gives the default, `false` unless you pass `true`:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
declare const command: Command
|
|
455
|
+
// ---cut---
|
|
456
|
+
if (await command.confirm('Do you wish to continue?', true)) {
|
|
457
|
+
// ...
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Multiple choice questions
|
|
462
|
+
|
|
463
|
+
`choice()` lists the choices, numbered from 0, and returns the one picked by its number or its text. The third
|
|
464
|
+
argument is the index of the default, returned for an empty answer:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
declare const command: Command
|
|
468
|
+
// ---cut---
|
|
469
|
+
const name = await command.choice('What is your name?', ['Taylor', 'Dayle'], 0)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
An answer that is neither stops the command, with ""Tom" is not a valid choice." and exit code `1`.
|
|
473
|
+
|
|
474
|
+
> [!NOTE]
|
|
475
|
+
> Laravel's `choice()` asks again after a wrong answer, and can take several answers; `anticipate()`, which suggests
|
|
476
|
+
> answers as you type, and Laravel Prompts aren't in Oriel.
|
|
477
|
+
|
|
478
|
+
### Writing output
|
|
479
|
+
|
|
480
|
+
`info()`, `comment()` and `line()` print a line to the standard output, and `warn()` and `error()` print one to the
|
|
481
|
+
standard error. `newLine()` prints a blank line, or as many as you ask for:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
declare const command: Command
|
|
485
|
+
// ---cut---
|
|
486
|
+
command.info('The command was successful!')
|
|
487
|
+
|
|
488
|
+
command.error('Something went wrong!')
|
|
489
|
+
|
|
490
|
+
command.line('Display this on the screen')
|
|
491
|
+
|
|
492
|
+
command.newLine()
|
|
493
|
+
command.newLine(3)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
> [!NOTE]
|
|
497
|
+
> Laravel prints each kind of message in its own colour. Oriel prints them as plain text; `warn()` and `error()` go to
|
|
498
|
+
> the standard error, so a script that runs your command can tell them apart.
|
|
499
|
+
|
|
500
|
+
#### Tables
|
|
501
|
+
|
|
502
|
+
`table()` takes the column headings and the rows, each a list of strings, and sizes the columns to fit. The rows are
|
|
503
|
+
a plain array, so end a collection's `map()` with `all()`:
|
|
504
|
+
|
|
505
|
+
```ts title="app/Console/Commands/ListUsers.ts"
|
|
506
|
+
import { Command } from '@orieljs/framework'
|
|
507
|
+
import { User } from '../../Models/User'
|
|
508
|
+
|
|
509
|
+
export class ListUsers extends Command {
|
|
510
|
+
readonly signature = 'users:list'
|
|
511
|
+
override description = 'List the users'
|
|
512
|
+
|
|
513
|
+
async handle(): Promise<void> {
|
|
514
|
+
const users = await User.all()
|
|
515
|
+
|
|
516
|
+
this.table(
|
|
517
|
+
['Name', 'Email'],
|
|
518
|
+
users.map((user) => [user.name, user.email]).all(),
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Laravel's progress bars aren't in Oriel.
|
|
525
|
+
|
|
526
|
+
## Registering commands
|
|
527
|
+
|
|
528
|
+
`oriel` registers every command class in `app/Console/Commands` and its subfolders: each exported class with a
|
|
529
|
+
`handle()` method and a `signature`. It then loads `routes/console.ts`, the file that `withRouting()` names as
|
|
530
|
+
`console` in `bootstrap/app.ts`, which defines your closure commands.
|
|
531
|
+
|
|
532
|
+
A command elsewhere is registered with `Artisan.register()`, in `routes/console.ts` or in a service provider's
|
|
533
|
+
`boot()`:
|
|
534
|
+
|
|
535
|
+
```ts title="routes/console.ts"
|
|
536
|
+
// @filename: app/Domain/Orders/Commands/SendEmails.ts
|
|
537
|
+
import { Command } from '@orieljs/framework'
|
|
538
|
+
export class SendEmails extends Command {
|
|
539
|
+
readonly signature = 'orders:send-emails'
|
|
540
|
+
handle(): void {}
|
|
541
|
+
}
|
|
542
|
+
// ---cut---
|
|
543
|
+
import { Artisan } from '@orieljs/framework'
|
|
544
|
+
import { SendEmails } from '../app/Domain/Orders/Commands/SendEmails'
|
|
545
|
+
|
|
546
|
+
Artisan.register(SendEmails)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
A command class has no constructor arguments: `oriel` makes it with `new`. Two commands with the same name are one
|
|
550
|
+
command, the one registered last, so a command of yours can take the place of one of the framework's.
|
|
551
|
+
|
|
552
|
+
> [!WARNING]
|
|
553
|
+
> Your commands in `app/Console/Commands` and `routes/console.ts` are loaded only when your app runs in the console:
|
|
554
|
+
> under `oriel`, which `node oriel serve` is too, and in your tests. An app served by `public/index.ts` doesn't load
|
|
555
|
+
> them, so `Artisan.call()` from one of its routes can't find them. A command registered in a service provider's
|
|
556
|
+
> `boot()` is there in both.
|
|
557
|
+
|
|
558
|
+
## Programmatically executing commands
|
|
559
|
+
|
|
560
|
+
`Artisan.call()` runs a command from your code, such as a route or a job. It takes the command's name and its
|
|
561
|
+
arguments and options, with each option's name written with its `--`, and returns the exit code:
|
|
562
|
+
|
|
563
|
+
```ts title="routes/web.ts"
|
|
564
|
+
import { Artisan, Route } from '@orieljs/framework'
|
|
565
|
+
|
|
566
|
+
Route.post('/user/{user}/mail', async (user: string) => {
|
|
567
|
+
const exitCode = await Artisan.call('mail:send', { user, '--queue': 'default' })
|
|
568
|
+
|
|
569
|
+
return { exitCode, output: Artisan.output() }
|
|
570
|
+
})
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The command's output isn't printed: `Artisan.output()` returns what the last call printed, the standard output then
|
|
574
|
+
the standard error. A command that throws makes `Artisan.call()` throw, and so does a name that no command has.
|
|
575
|
+
|
|
576
|
+
> [!NOTE]
|
|
577
|
+
> Laravel's `Artisan::call()` also takes the whole command as one string, `'mail:send 1 --queue=default'`. Oriel's
|
|
578
|
+
> takes the name only, with the input as an object. `Artisan::queue()` isn't in Oriel: dispatch a
|
|
579
|
+
> [job](queues.md) that calls the command.
|
|
580
|
+
|
|
581
|
+
A command run by `Artisan.call()` has no one to answer its questions: `ask()`, `confirm()` and the others throw.
|
|
582
|
+
|
|
583
|
+
#### Passing array values
|
|
584
|
+
|
|
585
|
+
An option array or an array argument takes a list:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
await Artisan.call('mail:send', { '--id': [5, 13] })
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### Passing boolean values
|
|
592
|
+
|
|
593
|
+
A switch takes `true` to pass it. `false` leaves it off:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
await Artisan.call('migrate:refresh', { '--force': true })
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Calling commands from other commands
|
|
600
|
+
|
|
601
|
+
Inside a command class, `this.call()` runs another command. Its output goes to your command's output, and it returns
|
|
602
|
+
the exit code:
|
|
603
|
+
|
|
604
|
+
```ts title="app/Console/Commands/SendEmails.ts"
|
|
605
|
+
import { Command } from '@orieljs/framework'
|
|
606
|
+
|
|
607
|
+
export class SendEmails extends Command {
|
|
608
|
+
readonly signature = 'mail:send {user} {--queue=}'
|
|
609
|
+
|
|
610
|
+
async handle(): Promise<void> {
|
|
611
|
+
await this.call('mail:send-receipt', { user: this.argument('user'), '--queue': 'default' })
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
> [!WARNING]
|
|
617
|
+
> `this.call()` with a name that no command has does nothing, and returns `undefined`. Laravel throws there. It lets
|
|
618
|
+
> a command call another that a part of the framework your app may not use would register, as `make:model -m` calls
|
|
619
|
+
> `make:migration`.
|
|
620
|
+
|
|
621
|
+
Laravel's `callSilently()` isn't in Oriel: call `Artisan.call()`, whose output isn't printed.
|
|
622
|
+
|
|
623
|
+
## Testing commands
|
|
624
|
+
|
|
625
|
+
Your feature tests run a command with `artisan()`, and check its output and its exit code. Await the result: the
|
|
626
|
+
command runs then, and not before.
|
|
627
|
+
|
|
628
|
+
```ts title="tests/Feature/SendEmails.test.ts"
|
|
629
|
+
// @filename: tests/TestCase.ts
|
|
630
|
+
import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
|
|
631
|
+
export class TestCase extends BaseTestCase {}
|
|
632
|
+
// ---cut---
|
|
633
|
+
import { test } from 'vitest'
|
|
634
|
+
import { TestCase } from '../TestCase'
|
|
635
|
+
|
|
636
|
+
const t = TestCase.boot()
|
|
637
|
+
|
|
638
|
+
test('the mail is sent', async () => {
|
|
639
|
+
await t.artisan('mail:send', { user: 1 }).expectsOutput('Sending email to: 1!').assertExitCode(0)
|
|
640
|
+
})
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
`expectsOutput()` wants a line of the output to equal its text, and `expectsOutputToContain()` the output to contain
|
|
644
|
+
it; `doesntExpectOutput()` and `doesntExpectOutputToContain()` want the opposite. `assertSuccessful()` and
|
|
645
|
+
`assertFailed()` check the exit code is `0`, or isn't.
|
|
646
|
+
|
|
647
|
+
> [!NOTE]
|
|
648
|
+
> Laravel's `expectsQuestion()` and `expectsConfirmation()` aren't in Oriel: a command that asks a question fails in
|
|
649
|
+
> a test. Give it the answer as an argument or an option instead.
|
|
650
|
+
|
|
651
|
+
The [Testing](testing.md) pages cover the rest.
|