@orieljs/framework 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +7 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/chunk-7ZQZYDOC.js +3253 -0
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +535 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +2202 -5952
- package/dist/index.js +4900 -944
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +19 -5
- 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-A5XRLEWM.js.map +0 -1
- package/dist/chunk-BSQKOLSK.js.map +0 -1
- package/dist/chunk-PM5YTG57.js +0 -880
- package/dist/chunk-PM5YTG57.js.map +0 -1
- package/dist/chunk-WNJP46B7.js.map +0 -1
- package/dist/index.d-DOlTeG-8.d.ts +0 -99
package/docs/database.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Database: getting started
|
|
2
|
+
|
|
3
|
+
Your app talks to SQLite, PostgreSQL or MySQL through one API: raw SQL with the `DB` facade, the query builder, and
|
|
4
|
+
your models.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
Oriel reaches your database through [Kysely](https://kysely.dev), with three drivers:
|
|
9
|
+
|
|
10
|
+
- **SQLite**, with `better-sqlite3` on Node and Bun's own `bun:sqlite` on Bun. A new app uses it, so you need no
|
|
11
|
+
database server to start.
|
|
12
|
+
- **PostgreSQL**, with `pg`.
|
|
13
|
+
- **MySQL**, with `mysql2`.
|
|
14
|
+
|
|
15
|
+
`pg` and `mysql2` aren't installed with the framework: install the one you need, `npm install pg` or
|
|
16
|
+
`npm install mysql2`. A driver is loaded the first time your app queries the database.
|
|
17
|
+
|
|
18
|
+
### Configuration
|
|
19
|
+
|
|
20
|
+
Your connections are in `config/database.ts`. `default` names the one your app uses, `sqlite` unless
|
|
21
|
+
`DB_CONNECTION` names another:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
declare const basePath: string
|
|
25
|
+
declare function join(...paths: string[]): string
|
|
26
|
+
// ---cut---
|
|
27
|
+
const database = {
|
|
28
|
+
default: env('DB_CONNECTION', 'sqlite'),
|
|
29
|
+
connections: {
|
|
30
|
+
sqlite: { driver: 'sqlite', database: env('DB_DATABASE', join(basePath, 'database/database.sqlite')) },
|
|
31
|
+
pgsql: { driver: 'pgsql', host: env('DB_HOST', '127.0.0.1'), port: Number(env('DB_PORT', 5432)), database: env('DB_DATABASE', 'oriel') },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each connection takes `host`, `port`, `database`, `username` and `password`, read from the `DB_*` variables of your
|
|
37
|
+
`.env` (see [Databases and migrations](installation.md#databases-and-migrations)). `pool.max` is the size of the
|
|
38
|
+
connection pool, 10 unless you set it. A `pgsql` connection takes a `search_path`, and a `mysql` one a `charset`,
|
|
39
|
+
`utf8mb4` unless you set it.
|
|
40
|
+
|
|
41
|
+
#### SQLite configuration
|
|
42
|
+
|
|
43
|
+
A SQLite connection's `database` is the path of the database file. A new app's is `database/database.sqlite`, as an
|
|
44
|
+
absolute path, since a relative one is opened from the folder your app runs in; `migrate` creates the file. `:memory:`
|
|
45
|
+
gives a database in memory, which your tests use.
|
|
46
|
+
|
|
47
|
+
SQLite checks foreign keys, as `foreign_key_constraints: true` asks; set it to `false` to turn the checks off. Changing
|
|
48
|
+
a table on SQLite rebuilds it: [SQLite and schema changes](migrations.md#sqlite-and-schema-changes) explains.
|
|
49
|
+
|
|
50
|
+
#### Configuration using URLs
|
|
51
|
+
|
|
52
|
+
A `pgsql` or `mysql` connection can take a single `url`, such as the one a hosting provider gives you, in `DB_URL`:
|
|
53
|
+
|
|
54
|
+
```dotenv
|
|
55
|
+
DB_URL=postgresql://user:secret@db.example.com:5432/blog
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A URL's parts win over the separate keys. A SQLite connection ignores `url`: give it a `database` path.
|
|
59
|
+
|
|
60
|
+
> [!NOTE]
|
|
61
|
+
> Laravel's read and write connections, its `sqlsrv` driver and a connection's table `prefix` aren't in Oriel. A new
|
|
62
|
+
> app's `config/database.ts` keeps a `prefix: ''` line from Laravel's file, which nothing reads.
|
|
63
|
+
|
|
64
|
+
## Running SQL queries
|
|
65
|
+
|
|
66
|
+
The `DB` facade runs raw SQL on the default connection. Every method is async.
|
|
67
|
+
|
|
68
|
+
#### Running a select query
|
|
69
|
+
|
|
70
|
+
`DB.select()` runs a `select`, with the values for its `?` placeholders in an array, and returns the rows as objects:
|
|
71
|
+
|
|
72
|
+
```ts title="app/Http/Controllers/UserController.ts"
|
|
73
|
+
import { DB } from '@orieljs/framework'
|
|
74
|
+
import { Controller } from './Controller'
|
|
75
|
+
|
|
76
|
+
export class UserController extends Controller {
|
|
77
|
+
/** Show a list of all of the application's users. */
|
|
78
|
+
async index() {
|
|
79
|
+
const users = await DB.select<{ id: number; name: string }>('select * from users where active = ?', [1])
|
|
80
|
+
|
|
81
|
+
return { users }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The values are bound, never written into the SQL, so they are safe from SQL injection. The type you give the rows is
|
|
87
|
+
a cast, not a check.
|
|
88
|
+
|
|
89
|
+
> [!NOTE]
|
|
90
|
+
> Laravel also takes named bindings (`:id`). Oriel takes `?` only, and finds them by splitting the SQL at each `?`,
|
|
91
|
+
> so a `?` inside a string, or PostgreSQL's `?` operator, counts as one too: pass such text as a binding.
|
|
92
|
+
|
|
93
|
+
#### Using multiple database connections
|
|
94
|
+
|
|
95
|
+
`DB.connection()` returns another connection of `config/database.ts`, with `select()`, `statement()`,
|
|
96
|
+
`affectingStatement()`, `table()` and `transaction()`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const users = await DB.connection('sqlite').select('select * from users')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`DB.usingConnection('sqlite', () => ...)` runs a function with another connection as the default, and puts the default
|
|
103
|
+
back after it.
|
|
104
|
+
|
|
105
|
+
#### Using Kysely
|
|
106
|
+
|
|
107
|
+
Oriel's queries run on Kysely. `DB.kysely()` returns the connection's Kysely instance, for a query the query builder
|
|
108
|
+
doesn't write; inside a transaction it is the transaction's, so its queries join it:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const rows = await DB.kysely().selectFrom('users').select(['id', 'name']).where('votes', '>', 100).execute()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Running an insert statement
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await DB.insert('insert into users (id, name) values (?, ?)', [1, 'Marc'])
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Running an update statement
|
|
121
|
+
|
|
122
|
+
`DB.update()` returns the number of rows it changed:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const affected = await DB.update('update users set votes = 100 where name = ?', ['Anita'])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Running a delete statement
|
|
129
|
+
|
|
130
|
+
`DB.delete()` returns the number of rows it deleted:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const deleted = await DB.delete('delete from users')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Running a general statement
|
|
137
|
+
|
|
138
|
+
`DB.statement()` runs SQL that returns nothing, such as a schema change, and `DB.affectingStatement()` returns the
|
|
139
|
+
number of rows a statement changed:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await DB.statement('drop table users')
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Raw expressions
|
|
146
|
+
|
|
147
|
+
`DB.raw()` makes an expression the query builder writes as it is, with its own bindings:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const users = await DB.table('users').select(DB.raw('count(*) as user_count, status')).groupBy('status').get()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A raw expression is SQL you write, so never put a user's input in it: bind it with `?` instead.
|
|
154
|
+
|
|
155
|
+
### Values in bindings
|
|
156
|
+
|
|
157
|
+
Oriel turns some values into what the database stores: `true` and `false` become `1` and `0` (except on PostgreSQL,
|
|
158
|
+
which has booleans), a `Date` becomes its UTC time as `'2026-01-02 03:04:05'`, an object or an array becomes JSON,
|
|
159
|
+
and `undefined` becomes `null`.
|
|
160
|
+
|
|
161
|
+
Dates come back as strings, on every driver: your models' date casts turn them into `Date` objects.
|
|
162
|
+
|
|
163
|
+
## Database transactions
|
|
164
|
+
|
|
165
|
+
`DB.transaction()` runs a function in a transaction. When the function throws, the transaction is rolled back and the
|
|
166
|
+
error is thrown again; otherwise it is committed:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await DB.transaction(async () => {
|
|
170
|
+
await DB.update('update users set votes = 1')
|
|
171
|
+
|
|
172
|
+
await DB.delete('delete from posts')
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The queries your function runs, through the facade, the query builder or your models, are in the transaction, and
|
|
177
|
+
those of other requests running at the same time aren't. A transaction inside another uses a savepoint, so an inner
|
|
178
|
+
one that throws rolls back only its own work. The function's result is `transaction()`'s:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const user = await DB.transaction(async () => {
|
|
182
|
+
const id = await DB.table('users').insertGetId({ name: 'Taylor', email: 'taylor@example.com', password: '' })
|
|
183
|
+
|
|
184
|
+
return DB.table('users').where('id', id).first()
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> [!NOTE]
|
|
189
|
+
> Laravel's `transaction()` takes a number of attempts for deadlocks as its second argument. Oriel's doesn't: catch
|
|
190
|
+
> the error and run it again yourself.
|
|
191
|
+
|
|
192
|
+
### Manually using transactions
|
|
193
|
+
|
|
194
|
+
`DB.beginTransaction()` starts a transaction that lasts, for every query on the connection, until `DB.commit()` or
|
|
195
|
+
`DB.rollBack()`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await DB.beginTransaction()
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await DB.update('update users set votes = 1')
|
|
202
|
+
await DB.commit()
|
|
203
|
+
} catch (error) {
|
|
204
|
+
await DB.rollBack()
|
|
205
|
+
throw error
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Prefer `transaction()`: a manual transaction takes in the queries of every request until it ends, which suits a
|
|
210
|
+
script or a test, not a request.
|
|
211
|
+
|
|
212
|
+
## Logging queries
|
|
213
|
+
|
|
214
|
+
`DB.enableQueryLog()` records each query the default connection runs, with its bindings and how many milliseconds it
|
|
215
|
+
took, and `DB.getQueryLog()` returns them:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
DB.enableQueryLog()
|
|
219
|
+
|
|
220
|
+
await DB.table('users').get()
|
|
221
|
+
|
|
222
|
+
const queries = DB.getQueryLog() // [{ query: 'select * from "users"', bindings: [], time: 0.4 }]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A query that fails throws a `QueryError`, whose message ends with the connection's name and the SQL, and which has
|
|
226
|
+
`sql` and `bindings` fields.
|
|
227
|
+
|
|
228
|
+
> [!NOTE]
|
|
229
|
+
> Laravel's `DB::listen()` and its cumulative query time warnings, the `db` command and the `db:show` and `db:table`
|
|
230
|
+
> commands aren't in Oriel.
|
|
231
|
+
|
|
232
|
+
## Closing connections
|
|
233
|
+
|
|
234
|
+
Your app's server, its `oriel` commands and its queue workers close their database connections when they shut down. A
|
|
235
|
+
script of your own can end with `await app.shutdown()`, or close them with `await DB.disconnectAll()`;
|
|
236
|
+
`DB.disconnect('mysql')` closes one.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
To run your app in production, build its assets, set its environment, and keep its server, queue workers and
|
|
4
|
+
scheduler running under a process manager.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
An Oriel app is a long-running process: one server answers every request, where a PHP app starts afresh for each one.
|
|
9
|
+
So deploying it means starting that process, keeping it running, and restarting it with each new version, along with
|
|
10
|
+
the processes beside it: the queue workers, the scheduler and, for the Inertia kits with server-side rendering, the
|
|
11
|
+
SSR server.
|
|
12
|
+
|
|
13
|
+
## Server requirements
|
|
14
|
+
|
|
15
|
+
Your server needs **Node 22** or later, or **Bun 1.2** or later. On Node, your app's TypeScript runs through `tsx`,
|
|
16
|
+
which every app has as a dependency; Bun runs it as it is. There is no compile step for your server code.
|
|
17
|
+
|
|
18
|
+
A PostgreSQL or MySQL database needs its driver in your dependencies, `pg` or `mysql2`. SQLite needs nothing more.
|
|
19
|
+
|
|
20
|
+
## Building your application
|
|
21
|
+
|
|
22
|
+
On each deploy, install the dependencies from your lockfile, build the assets, and run the migrations:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm ci
|
|
26
|
+
npm run build
|
|
27
|
+
node oriel migrate --force
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`npm run build` writes your pages' scripts and styles into `public/build`, and, in the Inertia kits with server-side
|
|
31
|
+
rendering, the SSR bundle into `bootstrap/ssr`. The API kit has no `build` script: skip that step. `migrate` asks
|
|
32
|
+
for `--force` in production, since it changes your database.
|
|
33
|
+
|
|
34
|
+
Leave `public/hot` off your server: Vite writes it while `npm run dev` runs, and while it exists, your pages load their
|
|
35
|
+
assets from a Vite dev server rather than from `public/build`. Git ignores it, so a deploy from your repository
|
|
36
|
+
doesn't have it.
|
|
37
|
+
|
|
38
|
+
## Configuring the environment
|
|
39
|
+
|
|
40
|
+
Give your server its own values, in its environment or in its own `.env` (see [Configuration](configuration.md)). At
|
|
41
|
+
least:
|
|
42
|
+
|
|
43
|
+
```dotenv
|
|
44
|
+
APP_ENV=production
|
|
45
|
+
APP_DEBUG=false
|
|
46
|
+
APP_KEY=base64:...
|
|
47
|
+
APP_URL=https://example.com
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`APP_KEY` must be the same on every server of the app, and stay the same from one deploy to the next: it decrypts
|
|
51
|
+
your cookies and sessions, and checks your signed URLs. Generate it once, with `node oriel key:generate --show`.
|
|
52
|
+
|
|
53
|
+
In production, `migrate` and the other commands that change your data need `--force`, `key:generate` won't replace a
|
|
54
|
+
key that is set, `migrate` doesn't rewrite `types/models.d.ts`, and the kits' dashboard hides its Getting started
|
|
55
|
+
checklist.
|
|
56
|
+
|
|
57
|
+
## Server configuration
|
|
58
|
+
|
|
59
|
+
### Running the server
|
|
60
|
+
|
|
61
|
+
`public/index.ts` is your app's production entry point. It creates your app and serves it:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
node --import tsx public/index.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On Bun, run `bun public/index.ts`. The server listens on `127.0.0.1`, on the port the `PORT` variable names, or
|
|
68
|
+
`SERVER_PORT`, or 8000. It serves with `node:http` on Node and `Bun.serve()` on Bun.
|
|
69
|
+
|
|
70
|
+
`node oriel serve` runs the same server through your app's command line, and takes `--host` and `--port`; `npm start`
|
|
71
|
+
runs it too, and so does the CI workflow the installer writes. Leave `--watch` out in production: it restarts the
|
|
72
|
+
server on every file change.
|
|
73
|
+
|
|
74
|
+
### Containers
|
|
75
|
+
|
|
76
|
+
In a container, the server must listen on every interface, not only on `127.0.0.1`, or nothing outside the container
|
|
77
|
+
reaches it. With `node oriel serve`, pass `--host=0.0.0.0`:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
node oriel serve --host=0.0.0.0 --port=8000
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
With `public/index.ts`, give `serve()` the host:
|
|
84
|
+
|
|
85
|
+
```ts title="public/index.ts"
|
|
86
|
+
// @filename: bootstrap/app.ts
|
|
87
|
+
import type { Application } from '@orieljs/framework'
|
|
88
|
+
export declare function createApplication(): Promise<Application>
|
|
89
|
+
// ---cut---
|
|
90
|
+
import { serve } from '@orieljs/framework'
|
|
91
|
+
import { createApplication } from '../bootstrap/app'
|
|
92
|
+
|
|
93
|
+
// Production entry: `node --import tsx public/index.ts`, or `bun public/index.ts`.
|
|
94
|
+
await serve(await createApplication(), { hostname: '0.0.0.0' })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`serve()` takes these options:
|
|
98
|
+
|
|
99
|
+
| Option | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `hostname` | The address to listen on (default: `127.0.0.1`) |
|
|
102
|
+
| `port` | The port (default: `PORT`, then `SERVER_PORT`, then 8000; 0 picks a free one) |
|
|
103
|
+
| `publicPath` | The folder whose files are sent as they are (default: `public`); `false` turns it off |
|
|
104
|
+
| `shutdownTimeout` | How long a shutdown waits for the requests in flight, in milliseconds (default: 10000) |
|
|
105
|
+
| `signals` | Whether SIGINT and SIGTERM shut the server down (default: `true`) |
|
|
106
|
+
|
|
107
|
+
Point your container's health check at the [health route](#the-health-route), `/up`.
|
|
108
|
+
|
|
109
|
+
### Static files
|
|
110
|
+
|
|
111
|
+
The server sends a file that exists in `public` before your routes run, so `public/build` and your icons need no
|
|
112
|
+
other web server. The built assets, under `/build/assets/`, are sent with a year-long, immutable cache header, as
|
|
113
|
+
their names change with their content. TypeScript files and dotfiles in `public` are never sent.
|
|
114
|
+
|
|
115
|
+
A web server or a CDN in front of your app can serve `public` itself; then pass `publicPath: false` to `serve()`.
|
|
116
|
+
|
|
117
|
+
### Behind a proxy
|
|
118
|
+
|
|
119
|
+
Behind a load balancer or a proxy that ends TLS, your app sees the proxy's request: plain HTTP, from the proxy's
|
|
120
|
+
address. List your proxies in `http.trusted_proxies`, in `config/http.ts`, so that `request().ip()`, `url()` and
|
|
121
|
+
`route()` use the client's address, scheme and host from the `X-Forwarded-*` headers.
|
|
122
|
+
[Configuring trusted proxies](requests.md#configuring-trusted-proxies) shows the settings.
|
|
123
|
+
|
|
124
|
+
### Directory permissions
|
|
125
|
+
|
|
126
|
+
Your app writes into `storage`: its logs, uploaded files, and the file cache and sessions when you use those drivers.
|
|
127
|
+
The user that runs it needs write access there, and in `database` when you use SQLite.
|
|
128
|
+
|
|
129
|
+
### Shutting down
|
|
130
|
+
|
|
131
|
+
On SIGTERM or SIGINT, the server stops taking new connections and lets the requests in flight finish, for up to 10
|
|
132
|
+
seconds (`shutdownTimeout`). Then the app shuts down: it closes the database pools and the Redis connections that the
|
|
133
|
+
cache, the queue and the session opened, and writes out its logs. Then the process exits with code 0. A process
|
|
134
|
+
manager that sends SIGTERM to stop or restart your app gets a clean exit, with no log line lost and no request cut
|
|
135
|
+
short, unless it takes longer than 10 seconds. When closing the handles takes more than 10 seconds, the rest are given
|
|
136
|
+
up on, so the process always exits.
|
|
137
|
+
|
|
138
|
+
The `oriel` commands shut down the same way when they finish, and a queue worker finishes its current job first.
|
|
139
|
+
|
|
140
|
+
## Optimization
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> Laravel caches its configuration, routes, events and views before it serves (`optimize`, `config:cache`,
|
|
144
|
+
> `route:cache`, `event:cache`, `view:cache`), since each PHP request would read them again. Oriel's process reads
|
|
145
|
+
> them once, when it starts, and keeps them: there is nothing to cache, and none of those commands exists.
|
|
146
|
+
|
|
147
|
+
## Queue workers
|
|
148
|
+
|
|
149
|
+
A queue worker is a process of its own: `node oriel queue:work` takes jobs off the queue until it is stopped (see
|
|
150
|
+
[Running the queue worker](queues.md#running-the-queue-worker)). Keep your workers running with a process manager,
|
|
151
|
+
such as Supervisor or systemd, which starts a new worker whenever one exits.
|
|
152
|
+
|
|
153
|
+
A worker stops by itself in a few cases, so that the process manager starts it again with a clean slate:
|
|
154
|
+
|
|
155
|
+
- `--memory`: when its JavaScript heap passes this many megabytes, 512 unless you set it (exit code 12).
|
|
156
|
+
- `--max-jobs` and `--max-time`: after that many jobs, or seconds (exit code 0).
|
|
157
|
+
- A job that runs past its timeout (exit code 1).
|
|
158
|
+
- `queue:restart` (exit code 0, after the current job).
|
|
159
|
+
|
|
160
|
+
Since a worker also exits with code 0, the process manager must restart it on every exit, not only on a failure. A
|
|
161
|
+
Supervisor program for two workers:
|
|
162
|
+
|
|
163
|
+
```ini
|
|
164
|
+
[program:blog-worker]
|
|
165
|
+
process_name=%(program_name)s_%(process_num)02d
|
|
166
|
+
command=node oriel queue:work --sleep=3 --tries=3 --memory=512 --max-time=3600
|
|
167
|
+
directory=/var/www/blog
|
|
168
|
+
autostart=true
|
|
169
|
+
autorestart=true
|
|
170
|
+
stopasgroup=true
|
|
171
|
+
killasgroup=true
|
|
172
|
+
user=www-data
|
|
173
|
+
numprocs=2
|
|
174
|
+
redirect_stderr=true
|
|
175
|
+
stdout_logfile=/var/www/blog/storage/logs/worker.log
|
|
176
|
+
stopwaitsecs=3600
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`stopwaitsecs` gives a worker time to finish its current job when Supervisor stops it: set it longer than your
|
|
180
|
+
longest job.
|
|
181
|
+
|
|
182
|
+
### Restarting the workers
|
|
183
|
+
|
|
184
|
+
A worker loads your code once, so after a deploy it still runs the old code. Tell every worker to stop after its
|
|
185
|
+
current job:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
node oriel queue:restart
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The process manager then starts each one again, on the new code. `queue:restart` leaves its signal in your app's
|
|
192
|
+
default cache store, which the workers read after each job, so the workers and the command must share that store:
|
|
193
|
+
the `database` store of a new app does, as long as they share the database. With the `array` store, a worker never
|
|
194
|
+
sees the signal. While the cache can't be read, a worker goes on working.
|
|
195
|
+
|
|
196
|
+
## Task scheduling
|
|
197
|
+
|
|
198
|
+
The scheduler runs the tasks you schedule in `routes/console.ts`. Run `schedule:run` every minute with cron, and it
|
|
199
|
+
runs the tasks that are due:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
* * * * * cd /var/www/blog && node oriel schedule:run >> /dev/null 2>&1
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Where you have no cron, as in many containers, run `node oriel schedule:work` under your process manager instead: it
|
|
206
|
+
runs the due tasks every minute until it is stopped.
|
|
207
|
+
|
|
208
|
+
## The SSR server
|
|
209
|
+
|
|
210
|
+
The React, Vue, Svelte and Solid kits render their pages on the server through a separate SSR server. Keep it running
|
|
211
|
+
beside your app, under your process manager:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
node oriel inertia:start-ssr
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
On Bun, add `--runtime=bun`. It runs the bundle that `npm run build` wrote into `bootstrap/ssr`, on the port of
|
|
218
|
+
`INERTIA_SSR_URL` (`http://127.0.0.1:13714` unless you set it), and exits when the bundle does. When no SSR server
|
|
219
|
+
answers, your pages are rendered in the browser instead, so a restart doesn't break them.
|
|
220
|
+
|
|
221
|
+
After a deploy, stop it, and let the process manager start it on the new bundle:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
node oriel inertia:stop-ssr
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The Angular kit, the Live kit and the API kit have no SSR server.
|
|
228
|
+
|
|
229
|
+
## Reloading services
|
|
230
|
+
|
|
231
|
+
After a deploy, every long-running process must restart to run the new code: your server, your queue workers and
|
|
232
|
+
your SSR server. Restart the server through your process manager, then:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
node oriel queue:restart
|
|
236
|
+
node oriel inertia:stop-ssr
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Laravel's `reload` command, which does both, isn't in Oriel.
|
|
240
|
+
|
|
241
|
+
## Debug mode
|
|
242
|
+
|
|
243
|
+
`debug` in `config/app.ts` reads `APP_DEBUG`, and decides how much an error response shows (see
|
|
244
|
+
[Error handling](errors.md#configuration)).
|
|
245
|
+
|
|
246
|
+
> [!WARNING]
|
|
247
|
+
> In production, `APP_DEBUG` must be `false`. The debug page shows your source code and your error messages, which
|
|
248
|
+
> can hold secrets, to anyone who triggers an error.
|
|
249
|
+
|
|
250
|
+
## The health route
|
|
251
|
+
|
|
252
|
+
A new app answers `/up` with a small page and a 200 when it is running, for your uptime monitor, your load
|
|
253
|
+
balancer or your container's health check. The route is outside the `web` middleware group, so it starts no session.
|
|
254
|
+
`health` in `withRouting()`, in `bootstrap/app.ts`, sets its URI:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
Application.configure({ basePath: '.' }).withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/status' })
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Each request to it dispatches the `DiagnosingHealth` event. A listener can check what your app needs, such as its
|
|
261
|
+
database, and throw when it fails:
|
|
262
|
+
|
|
263
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
264
|
+
import { DB, DiagnosingHealth, Event, ServiceProvider } from '@orieljs/framework'
|
|
265
|
+
|
|
266
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
267
|
+
override boot(): void {
|
|
268
|
+
Event.listen(DiagnosingHealth, async () => {
|
|
269
|
+
await DB.select('select 1')
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
When a listener throws, the route answers 500, "Application experiencing problems", and the error is reported. With
|
|
276
|
+
`APP_DEBUG=true`, the error page shows the error instead.
|