@spinajs/http 2.0.486 → 2.0.487
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 +370 -5
- package/lib/cjs/base-controller.d.ts +35 -0
- package/lib/cjs/base-controller.d.ts.map +1 -0
- package/lib/cjs/base-controller.js +100 -0
- package/lib/cjs/base-controller.js.map +1 -0
- package/lib/cjs/cache.d.ts +25 -3
- package/lib/cjs/cache.d.ts.map +1 -1
- package/lib/cjs/cache.js +26 -9
- package/lib/cjs/cache.js.map +1 -1
- package/lib/cjs/cli/GenerateControllerCache.d.ts +13 -4
- package/lib/cjs/cli/GenerateControllerCache.d.ts.map +1 -1
- package/lib/cjs/cli/GenerateControllerCache.js +37 -10
- package/lib/cjs/cli/GenerateControllerCache.js.map +1 -1
- package/lib/cjs/controller-sources.d.ts +36 -0
- package/lib/cjs/controller-sources.d.ts.map +1 -0
- package/lib/cjs/controller-sources.js +73 -0
- package/lib/cjs/controller-sources.js.map +1 -0
- package/lib/cjs/controllers.d.ts +19 -36
- package/lib/cjs/controllers.d.ts.map +1 -1
- package/lib/cjs/controllers.js +98 -302
- package/lib/cjs/controllers.js.map +1 -1
- package/lib/cjs/exceptions.d.ts +12 -0
- package/lib/cjs/exceptions.d.ts.map +1 -1
- package/lib/cjs/exceptions.js +15 -1
- package/lib/cjs/exceptions.js.map +1 -1
- package/lib/cjs/index.d.ts +4 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +4 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/middlewares/RequestId.d.ts.map +1 -1
- package/lib/cjs/middlewares/RequestId.js +4 -0
- package/lib/cjs/middlewares/RequestId.js.map +1 -1
- package/lib/cjs/route-builder.d.ts +61 -0
- package/lib/cjs/route-builder.d.ts.map +1 -0
- package/lib/cjs/route-builder.js +228 -0
- package/lib/cjs/route-builder.js.map +1 -0
- package/lib/mjs/base-controller.d.ts +35 -0
- package/lib/mjs/base-controller.d.ts.map +1 -0
- package/lib/mjs/base-controller.js +93 -0
- package/lib/mjs/base-controller.js.map +1 -0
- package/lib/mjs/cache.d.ts +25 -3
- package/lib/mjs/cache.d.ts.map +1 -1
- package/lib/mjs/cache.js +25 -10
- package/lib/mjs/cache.js.map +1 -1
- package/lib/mjs/cli/GenerateControllerCache.d.ts +13 -4
- package/lib/mjs/cli/GenerateControllerCache.d.ts.map +1 -1
- package/lib/mjs/cli/GenerateControllerCache.js +39 -12
- package/lib/mjs/cli/GenerateControllerCache.js.map +1 -1
- package/lib/mjs/controller-sources.d.ts +36 -0
- package/lib/mjs/controller-sources.d.ts.map +1 -0
- package/lib/mjs/controller-sources.js +69 -0
- package/lib/mjs/controller-sources.js.map +1 -0
- package/lib/mjs/controllers.d.ts +19 -36
- package/lib/mjs/controllers.d.ts.map +1 -1
- package/lib/mjs/controllers.js +95 -298
- package/lib/mjs/controllers.js.map +1 -1
- package/lib/mjs/exceptions.d.ts +12 -0
- package/lib/mjs/exceptions.d.ts.map +1 -1
- package/lib/mjs/exceptions.js +12 -0
- package/lib/mjs/exceptions.js.map +1 -1
- package/lib/mjs/index.d.ts +4 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +4 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/middlewares/RequestId.d.ts.map +1 -1
- package/lib/mjs/middlewares/RequestId.js +4 -0
- package/lib/mjs/middlewares/RequestId.js.map +1 -1
- package/lib/mjs/route-builder.d.ts +61 -0
- package/lib/mjs/route-builder.d.ts.map +1 -0
- package/lib/mjs/route-builder.js +216 -0
- package/lib/mjs/route-builder.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,11 +1,376 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @spinajs/http
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HTTP server & controller framework for SpinaJS, built on top of Express.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Class-based controllers with decorator routing (`@Get`, `@Post`, …)
|
|
6
|
+
- Declarative route arguments (`@Query`, `@Body`, `@Param`, `@File`, …) with validation & hydration
|
|
7
|
+
- Policies (authorization) and route middlewares
|
|
8
|
+
- Pluggable controller discovery (`ControllerSource`) — filesystem scan, DI registry, or your own
|
|
9
|
+
- Typed response classes (`Ok`, `Created`, `NotFound`, …) with content negotiation (JSON / HTML / XML)
|
|
10
|
+
- Controller metadata cache with ahead-of-time CLI build (fast cold starts in docker)
|
|
11
|
+
- Fail-fast startup: broken controllers throw typed exceptions instead of half-starting
|
|
6
12
|
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @spinajs/http
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/controllers/UsersController.ts
|
|
23
|
+
import { BaseController, BasePath, Get, Post, Ok, Created, NotFound, Query, Param, Body } from '@spinajs/http';
|
|
24
|
+
|
|
25
|
+
@BasePath('users')
|
|
26
|
+
export class UsersController extends BaseController {
|
|
27
|
+
/**
|
|
28
|
+
* GET /users?page=1
|
|
29
|
+
*/
|
|
30
|
+
@Get('/')
|
|
31
|
+
public async list(@Query() page?: number) {
|
|
32
|
+
return new Ok([{ id: 1, name: 'John' }]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GET /users/42
|
|
37
|
+
*/
|
|
38
|
+
@Get(':id')
|
|
39
|
+
public async get(@Param() id: number) {
|
|
40
|
+
if (id !== 42) {
|
|
41
|
+
return new NotFound({ message: 'no such user' });
|
|
42
|
+
}
|
|
43
|
+
return new Ok({ id, name: 'John' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* POST /users { "name": "Alice" }
|
|
48
|
+
*/
|
|
49
|
+
@Post('/')
|
|
50
|
+
public async create(@Body() user: UserDto) {
|
|
51
|
+
return new Created(user);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// src/index.ts — application bootstrap
|
|
58
|
+
import { DI } from '@spinajs/di';
|
|
59
|
+
import { Configuration } from '@spinajs/configuration';
|
|
60
|
+
import { fsService } from '@spinajs/fs';
|
|
61
|
+
import { Controllers, HttpServer } from '@spinajs/http';
|
|
62
|
+
|
|
63
|
+
await DI.resolve(Configuration);
|
|
64
|
+
await DI.resolve(fsService);
|
|
65
|
+
await DI.resolve(Controllers); // discovers & mounts all controllers
|
|
66
|
+
|
|
67
|
+
const server = await DI.resolve(HttpServer);
|
|
68
|
+
server.start(); // listens on http.port ( default 1337 )
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Controllers are auto-discovered from directories configured at `system.dirs.controllers`.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
All settings live under the `http` config key ( see `src/config/http.ts` for full defaults ):
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// config/http.ts ( app override )
|
|
79
|
+
import config from './config.js';
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
system: {
|
|
83
|
+
dirs: {
|
|
84
|
+
// where controller classes are scanned from
|
|
85
|
+
controllers: ['/app/dist/controllers'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
http: {
|
|
89
|
+
port: 3000,
|
|
90
|
+
|
|
91
|
+
// global prefix added to EVERY controller route, eg. api/v1 -> /api/v1/users
|
|
92
|
+
controllers: {
|
|
93
|
+
route: { prefix: 'api/v1' },
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// raw express middlewares, executed before routing
|
|
97
|
+
middlewares: [ /* helmet(), express.json(), ... */ ],
|
|
98
|
+
|
|
99
|
+
// signed cookie secret — ALWAYS override in production
|
|
100
|
+
cookie: {
|
|
101
|
+
secret: 'change-me',
|
|
102
|
+
options: { maxAge: 900000, httpOnly: true },
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// static content: GET /_static/* served from Path
|
|
106
|
+
Static: [{ Route: '/_static', Path: '/app/public' }],
|
|
107
|
+
|
|
108
|
+
ssl: { key: '', cert: '' },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Routing
|
|
114
|
+
|
|
115
|
+
Route decorators: `@Get`, `@Post`, `@Put`, `@Patch`, `@Del`, `@Head` — all take optional path and schema.
|
|
116
|
+
|
|
117
|
+
Path resolution rules:
|
|
118
|
+
|
|
119
|
+
| Declaration | Resulting path |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `@BasePath('user')` + `@Get()` on `refresh()` | `/user/refresh` ( method name fallback ) |
|
|
122
|
+
| `@BasePath('user')` + `@Get('/')` | `/user` |
|
|
123
|
+
| `@BasePath('user')` + `@Get('grants/:id')` | `/user/grants/:id` |
|
|
124
|
+
| no `@BasePath` | controller class name lowercased |
|
|
125
|
+
| config `http.controllers.route.prefix = 'api/v1'` | `/api/v1/...` prepended to all of the above |
|
|
126
|
+
|
|
127
|
+
## Route arguments
|
|
128
|
+
|
|
129
|
+
Declared per-parameter with decorators; extracted, validated and hydrated before the action runs:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
Get, Post, Query, Body, Param, Header, Cookie, Form, File, CsvFile, JsonFile,
|
|
134
|
+
FromXml, RawBody, Req, Res, Ip, RequestId, UserAgent, Referer, FromDI, PKey, Uuid,
|
|
135
|
+
} from '@spinajs/http';
|
|
136
|
+
|
|
137
|
+
class ExamplesController extends BaseController {
|
|
138
|
+
@Get(':id')
|
|
139
|
+
public async byId(@PKey() id: number) { /* primary key helper */ }
|
|
140
|
+
|
|
141
|
+
@Get('search')
|
|
142
|
+
public async search(@Query() q: string, @Header('x-api-key') key: string) { }
|
|
143
|
+
|
|
144
|
+
@Post('upload')
|
|
145
|
+
public async upload(@File({ maxFileSize: 1024 * 1024 }) file: IUploadedFile) { }
|
|
146
|
+
|
|
147
|
+
@Post('import')
|
|
148
|
+
public async import(@CsvFile() rows: unknown[]) { }
|
|
149
|
+
|
|
150
|
+
@Post('webhook')
|
|
151
|
+
public async webhook(@RawBody() raw: Buffer, @Header('x-signature') sig: string) {
|
|
152
|
+
// raw = exact received bytes, for signature verification
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Get('whoami')
|
|
156
|
+
public async whoami(@Ip() ip: string, @UserAgent() ua: string, @RequestId() rid: string) { }
|
|
157
|
+
|
|
158
|
+
@Get('svc')
|
|
159
|
+
public async svc(@FromDI() service: SomeService) { /* resolved from DI per request */ }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Selected argument decorators:
|
|
164
|
+
|
|
165
|
+
| Decorator | Source |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `@Query(schema?)` | query string parameter |
|
|
168
|
+
| `@Body(options?)` | JSON body ( whole body or single field ) |
|
|
169
|
+
| `@Param(schema?)` | URL parameter ( `:id` ) |
|
|
170
|
+
| `@Header(name?)` | request header |
|
|
171
|
+
| `@Cookie(secure?)` | cookie ( optionally signed ) |
|
|
172
|
+
| `@Form` / `@FormField` | multipart form data |
|
|
173
|
+
| `@File` / `@Files` | uploaded file(s), with size limits & upload middlewares |
|
|
174
|
+
| `@CsvFile` / `@JsonFile` | uploaded file parsed to data |
|
|
175
|
+
| `@FromXml` | XML request body, parsed |
|
|
176
|
+
| `@RawBody` | raw request bytes ( webhook signatures ) |
|
|
177
|
+
| `@Req` / `@Res` | express request / response |
|
|
178
|
+
| `@Ip`, `@RequestId`, `@UserAgent`, `@Referer` | request metadata |
|
|
179
|
+
| `@FromDI` | DI-resolved service |
|
|
180
|
+
| `@PKey`, `@Uuid` | validated identifier helpers |
|
|
181
|
+
| `@Model(Type)` | ORM model lookup ( with `@spinajs/orm-http` ) |
|
|
182
|
+
|
|
183
|
+
Custom types passed to `@Body` / `@Query` are hydrated: class instances are constructed and (optionally) validated against JSON schema attached via `@Schema` from `@spinajs/validation`. Custom hydration via `@Hydrator(MyHydrator)` on the DTO class.
|
|
184
|
+
|
|
185
|
+
## Responses
|
|
186
|
+
|
|
187
|
+
Actions return response objects ( content negotiation JSON / HTML / XML happens automatically based on `Accept` header ):
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { Ok, Created, NoContent, BadRequestResponse, Unauthorized, ForbiddenResponse,
|
|
191
|
+
NotFound, Conflict, ValidationError, ServerError, Json, Xml,
|
|
192
|
+
FileResponse, ZipResponse, JsonFileResponse, TemplateResponse, Redirect } from '@spinajs/http';
|
|
193
|
+
|
|
194
|
+
@Get('download')
|
|
195
|
+
public async download() {
|
|
196
|
+
return new FileResponse({ path: '/data/report.pdf', filename: 'report.pdf' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Get('page')
|
|
200
|
+
public async page() {
|
|
201
|
+
// renders pug template ( with @spinajs/templates-pug )
|
|
202
|
+
return new TemplateResponse('page.pug', { title: 'Hello' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Get('legacy')
|
|
206
|
+
public async legacy() {
|
|
207
|
+
return new Redirect('/new-location');
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Policies ( authorization )
|
|
212
|
+
|
|
213
|
+
Policies gate route execution. When several policies are attached, **one success is enough** — this allows alternative access paths ( e.g. session cookie OR api token ):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { BasePolicy, Policy, IRoute, IController } from '@spinajs/http';
|
|
217
|
+
|
|
218
|
+
export class ApiKeyPolicy extends BasePolicy {
|
|
219
|
+
public isEnabled(_route: IRoute, _controller: IController): boolean {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public async execute(req: express.Request): Promise<void> {
|
|
224
|
+
if (req.headers['x-api-key'] !== process.env.API_KEY) {
|
|
225
|
+
throw new Forbidden('invalid api key');
|
|
226
|
+
}
|
|
227
|
+
// resolving = access granted
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@BasePath('admin')
|
|
232
|
+
@Policy(ApiKeyPolicy) // controller-wide
|
|
233
|
+
export class AdminController extends BaseController {
|
|
234
|
+
@Get()
|
|
235
|
+
@Policy(SessionPolicy) // route-level, OR-ed with ApiKeyPolicy
|
|
236
|
+
public async dashboard() { ... }
|
|
237
|
+
}
|
|
7
238
|
```
|
|
8
|
-
const http = require('http');
|
|
9
239
|
|
|
10
|
-
|
|
240
|
+
Policies can also be referenced **by configuration key** — the key must resolve to a registered policy type name:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
@Policy('rbac.session.policy') // read from configuration at startup
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A config key that does not resolve to a registered `BasePolicy` throws `RouteRegistrationException` at startup — a silently dropped policy would leave the route unprotected.
|
|
247
|
+
|
|
248
|
+
## Route middlewares
|
|
249
|
+
|
|
250
|
+
Run before / after actions and can inspect the produced response:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { RouteMiddleware, Middleware } from '@spinajs/http';
|
|
254
|
+
|
|
255
|
+
export class AuditMiddleware extends RouteMiddleware {
|
|
256
|
+
public isEnabled(route: IRoute, controller: IController): boolean { return true; }
|
|
257
|
+
public async onBefore(req, res, route, controller): Promise<void> { /* before action */ }
|
|
258
|
+
public async onResponse(response, route, controller): Promise<void> { /* inspect response object */ }
|
|
259
|
+
public async onAfter(req, res, route, controller): Promise<void> { /* after action */ }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@Middleware(AuditMiddleware) // controller-wide or per-route
|
|
263
|
+
export class OrdersController extends BaseController { ... }
|
|
11
264
|
```
|
|
265
|
+
|
|
266
|
+
Server-level middlewares ( whole express stack, not per-route ) ship in `src/middlewares/`: `AccessLog`, `Cors`, `Compression`, `RequestId` ( w3c traceparent + `x-request-id` ), `ResponseTime`, `RealIp`, `ServerTiming`, `PerfRollup`, `SlowRequestWarning`, `NotFound`, `ErrorHandler`.
|
|
267
|
+
|
|
268
|
+
## Controller discovery ( ControllerSource )
|
|
269
|
+
|
|
270
|
+
Discovery is pluggable. Built-in sources:
|
|
271
|
+
|
|
272
|
+
- `FilesystemControllerSource` — scans `system.dirs.controllers` directories
|
|
273
|
+
- `DiRegistryControllerSource` — picks up types registered in DI **before** `Controllers` resolves:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// package bootstrapper — conditional controller registration
|
|
277
|
+
@Injectable(Bootstrapper)
|
|
278
|
+
export class MyPackageBootstrapper extends Bootstrapper {
|
|
279
|
+
public bootstrap(): void {
|
|
280
|
+
if (someFeatureFlag) {
|
|
281
|
+
DI.register(MyFeatureController).as(BaseController);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Custom source — implement and register, the loader picks it up automatically:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { ControllerSource, BaseController } from '@spinajs/http';
|
|
291
|
+
import { ClassInfo, Injectable } from '@spinajs/di';
|
|
292
|
+
|
|
293
|
+
@Injectable(ControllerSource)
|
|
294
|
+
export class PluginManifestSource extends ControllerSource {
|
|
295
|
+
public async getControllers(): Promise<Array<ClassInfo<BaseController>>> {
|
|
296
|
+
// read your plugin manifest, return ClassInfo entries ( name, type, file )
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Overriding a package controller
|
|
303
|
+
|
|
304
|
+
Register the subclass as an override — only the subclass mounts:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
DI.register(MyUserController).as(PackageUserController);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Subclassing a scanned controller **without** registering the override mounts BOTH and logs a warning ( express route order decides which answers ).
|
|
311
|
+
|
|
312
|
+
### Dynamic registration at runtime
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const controllers = await DI.resolve(Controllers);
|
|
316
|
+
await controllers.add(LateBoundController); // idempotent, mounts immediately
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Startup error handling
|
|
320
|
+
|
|
321
|
+
Registration is fail-fast — the app refuses to start instead of silently skipping broken pieces:
|
|
322
|
+
|
|
323
|
+
| Condition | Exception |
|
|
324
|
+
| --- | --- |
|
|
325
|
+
| controller instance could not be resolved | `ControllerRegistrationException` |
|
|
326
|
+
| controller has descriptor but no router ( `super.resolve()` not called ) | `ControllerRegistrationException` |
|
|
327
|
+
| route declared for a member that does not exist | `RouteRegistrationException` |
|
|
328
|
+
| unknown route type ( broken decorator ) | `RouteRegistrationException` |
|
|
329
|
+
| string policy config key not resolvable | `RouteRegistrationException` |
|
|
330
|
+
|
|
331
|
+
Routes inherited from a base class declared in another file are fine — parameter names fall back to runtime extraction.
|
|
332
|
+
|
|
333
|
+
## Controller cache & CLI
|
|
334
|
+
|
|
335
|
+
Route parameter names and JSDoc documentation ( used by `@spinajs/http-swagger` ) are extracted from `.d.ts` files with the TypeScript compiler and cached under `__cache__/__controllers__` ( configurable via the `__fs_controller_cache__` fs provider ). Entries are keyed by source file content hash — changed files regenerate automatically.
|
|
336
|
+
|
|
337
|
+
First app start pays the parsing cost. To avoid that ( e.g. docker images ), pre-build the cache at image build time:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# generate missing cache entries
|
|
341
|
+
spinajs http:controllers:cache
|
|
342
|
+
|
|
343
|
+
# force regeneration even if entries exist
|
|
344
|
+
spinajs http:controllers:cache --rebuild
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```dockerfile
|
|
348
|
+
FROM node:22 AS build
|
|
349
|
+
WORKDIR /app
|
|
350
|
+
COPY . .
|
|
351
|
+
RUN npm ci && npm run build
|
|
352
|
+
# pre-build controllers cache AFTER tsc — cache keys are content hashes of compiled .d.ts
|
|
353
|
+
RUN node node_modules/.bin/spinajs http:controllers:cache --rebuild
|
|
354
|
+
|
|
355
|
+
FROM node:22-slim
|
|
356
|
+
WORKDIR /app
|
|
357
|
+
COPY --from=build /app .
|
|
358
|
+
CMD ["node", "dist/index.js"]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The command exits non-zero when any controller fails to parse, failing the image build loudly.
|
|
362
|
+
|
|
363
|
+
## Exceptions
|
|
364
|
+
|
|
365
|
+
| Exception | Purpose |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| `ControllerRegistrationException` | controller-level startup failure |
|
|
368
|
+
| `RouteRegistrationException` | route-level startup failure |
|
|
369
|
+
| `EntityTooLargeException` | uploaded file exceeds limits ( maps to HTTP 413 ) |
|
|
370
|
+
|
|
371
|
+
## Related packages
|
|
372
|
+
|
|
373
|
+
- `@spinajs/http-swagger` — OpenAPI document generation from controller JSDoc
|
|
374
|
+
- `@spinajs/orm-http` — `@Model()` route arg, ORM-aware responses
|
|
375
|
+
- `@spinajs/rbac-http` — session / role policies
|
|
376
|
+
- `@spinajs/templates-pug` — HTML rendering for `TemplateResponse` and error pages
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import Express from 'express';
|
|
3
|
+
import { AsyncService, IContainer } from '@spinajs/di';
|
|
4
|
+
import { Log } from '@spinajs/log';
|
|
5
|
+
import { DataValidator } from '@spinajs/validation';
|
|
6
|
+
import { Configuration } from '@spinajs/configuration';
|
|
7
|
+
import { IController, IControllerDescriptor, IActionLocalStoregeContext } from './interfaces.js';
|
|
8
|
+
export declare abstract class BaseController extends AsyncService implements IController {
|
|
9
|
+
/**
|
|
10
|
+
* Array index getter
|
|
11
|
+
*/
|
|
12
|
+
[action: string]: any;
|
|
13
|
+
protected _router: Express.Router;
|
|
14
|
+
protected _container: IContainer;
|
|
15
|
+
protected _validator: DataValidator;
|
|
16
|
+
protected _log: Log;
|
|
17
|
+
protected _actionLocalStorage: AsyncLocalStorage<IActionLocalStoregeContext>;
|
|
18
|
+
protected _cfg: Configuration;
|
|
19
|
+
/**
|
|
20
|
+
* Express router with middleware stack
|
|
21
|
+
*/
|
|
22
|
+
get Router(): Express.Router;
|
|
23
|
+
/**
|
|
24
|
+
* Controller descriptor
|
|
25
|
+
*/
|
|
26
|
+
get Descriptor(): IControllerDescriptor;
|
|
27
|
+
/**
|
|
28
|
+
* Base path for all controller routes eg. my/custom/path/
|
|
29
|
+
*
|
|
30
|
+
* It can be defined via `@BasePath` decorator, defaults to controller name without `Controller` part.
|
|
31
|
+
*/
|
|
32
|
+
get BasePath(): string;
|
|
33
|
+
resolve(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=base-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-controller.d.ts","sourceRoot":"","sources":["../../src/base-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,YAAY,EAAE,UAAU,EAAyB,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAU,GAAG,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAOjG,8BAAsB,cAAe,SAAQ,YAAa,YAAW,WAAW;IAC9E;;OAEG;IACH,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC;IAEtB,SAAS,CAAC,OAAO,EAAG,OAAO,CAAC,MAAM,CAAC;IAGnC,SAAS,CAAC,UAAU,EAAG,UAAU,CAAC;IAGlC,SAAS,CAAC,UAAU,EAAG,aAAa,CAAC;IAGrC,SAAS,CAAC,IAAI,EAAG,GAAG,CAAC;IAGrB,SAAS,CAAC,mBAAmB,EAAG,iBAAiB,CAAC,0BAA0B,CAAC,CAAC;IAG9E,SAAS,CAAC,IAAI,EAAG,aAAa,CAAC;IAE/B;;OAEG;IACH,IAAW,MAAM,IAAI,OAAO,CAAC,MAAM,CAElC;IAED;;OAEG;IACH,IAAW,UAAU,IAAI,qBAAqB,CAE7C;IAED;;;;OAIG;IACH,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAEY,OAAO;CAsCrB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.BaseController = void 0;
|
|
16
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const di_1 = require("@spinajs/di");
|
|
19
|
+
const log_1 = require("@spinajs/log");
|
|
20
|
+
const validation_1 = require("@spinajs/validation");
|
|
21
|
+
const configuration_1 = require("@spinajs/configuration");
|
|
22
|
+
const decorators_js_1 = require("./decorators.js");
|
|
23
|
+
const exceptions_js_1 = require("./exceptions.js");
|
|
24
|
+
const response_js_1 = require("./response.js");
|
|
25
|
+
const error_js_1 = require("./error.js");
|
|
26
|
+
const route_builder_js_1 = require("./route-builder.js");
|
|
27
|
+
class BaseController extends di_1.AsyncService {
|
|
28
|
+
/**
|
|
29
|
+
* Express router with middleware stack
|
|
30
|
+
*/
|
|
31
|
+
get Router() {
|
|
32
|
+
return this._router;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Controller descriptor
|
|
36
|
+
*/
|
|
37
|
+
get Descriptor() {
|
|
38
|
+
return Reflect.getMetadata(decorators_js_1.CONTROLLED_DESCRIPTOR_SYMBOL, this);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Base path for all controller routes eg. my/custom/path/
|
|
42
|
+
*
|
|
43
|
+
* It can be defined via `@BasePath` decorator, defaults to controller name without `Controller` part.
|
|
44
|
+
*/
|
|
45
|
+
get BasePath() {
|
|
46
|
+
return this.Descriptor.BasePath ? this.Descriptor.BasePath : this.constructor.name.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
async resolve() {
|
|
49
|
+
await super.resolve();
|
|
50
|
+
if (!this.Descriptor) {
|
|
51
|
+
this._log.warn(`Controller ${this.constructor.name} does not have descriptor. If its abstract or base class ignore this message.`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this._router = express_1.default.Router();
|
|
55
|
+
for (const [, route] of this.Descriptor.Routes) {
|
|
56
|
+
// Fail fast: an unknown route type means the route decorator never set a
|
|
57
|
+
// valid express method — mounting the rest of the controller while
|
|
58
|
+
// silently dropping this route would hide the bug.
|
|
59
|
+
if (route.InternalType === 'unknown') {
|
|
60
|
+
throw new exceptions_js_1.RouteRegistrationException(`Unknown route type for ${this.constructor.name}::${String(route.Method)}`);
|
|
61
|
+
}
|
|
62
|
+
const path = (0, route_builder_js_1.buildRoutePath)(this.BasePath, route, this._cfg.get('http.controllers.route.prefix'));
|
|
63
|
+
const middlewares = await (0, route_builder_js_1.resolveRouteMiddlewares)(this.Descriptor, route, this._container);
|
|
64
|
+
const policies = await (0, route_builder_js_1.resolveRoutePolicies)(this.Descriptor, route, this._container, this._cfg, this._log, this.constructor.name, path);
|
|
65
|
+
const enabledMiddlewares = middlewares.filter((m) => m.isEnabled(route, this));
|
|
66
|
+
this._log.trace(`Registering route ${route.Type.toUpperCase()} ${this.constructor.name}::${String(route.Method)} at ${path}`);
|
|
67
|
+
const handlers = [
|
|
68
|
+
(0, route_builder_js_1.createPolicyGate)(policies, route, this, this._log),
|
|
69
|
+
...enabledMiddlewares.map((m) => (0, route_builder_js_1.wrapMiddlewareAction)(m, m.onBefore.bind(m), route, this)),
|
|
70
|
+
(0, route_builder_js_1.createActionHandler)(this, route, enabledMiddlewares, this._actionLocalStorage),
|
|
71
|
+
...enabledMiddlewares.map((m) => (0, route_builder_js_1.wrapMiddlewareAction)(m, m.onAfter.bind(m), route, this)),
|
|
72
|
+
(0, response_js_1.__handle_response__)(),
|
|
73
|
+
(0, error_js_1.__handle_error__)(),
|
|
74
|
+
];
|
|
75
|
+
this._router[route.InternalType](path, handlers);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.BaseController = BaseController;
|
|
80
|
+
__decorate([
|
|
81
|
+
(0, di_1.Autoinject)(di_1.Container),
|
|
82
|
+
__metadata("design:type", Object)
|
|
83
|
+
], BaseController.prototype, "_container", void 0);
|
|
84
|
+
__decorate([
|
|
85
|
+
(0, di_1.Autoinject)(),
|
|
86
|
+
__metadata("design:type", validation_1.DataValidator)
|
|
87
|
+
], BaseController.prototype, "_validator", void 0);
|
|
88
|
+
__decorate([
|
|
89
|
+
(0, log_1.Logger)('http'),
|
|
90
|
+
__metadata("design:type", log_1.Log)
|
|
91
|
+
], BaseController.prototype, "_log", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, di_1.Autoinject)(),
|
|
94
|
+
__metadata("design:type", node_async_hooks_1.AsyncLocalStorage)
|
|
95
|
+
], BaseController.prototype, "_actionLocalStorage", void 0);
|
|
96
|
+
__decorate([
|
|
97
|
+
(0, di_1.Autoinject)(configuration_1.Configuration),
|
|
98
|
+
__metadata("design:type", configuration_1.Configuration)
|
|
99
|
+
], BaseController.prototype, "_cfg", void 0);
|
|
100
|
+
//# sourceMappingURL=base-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-controller.js","sourceRoot":"","sources":["../../src/base-controller.ts"],"names":[],"mappings":"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"}
|
package/lib/cjs/cache.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { AsyncService, ClassInfo } from '@spinajs/di';
|
|
3
3
|
import { fs as fFs, FileHasher } from '@spinajs/fs';
|
|
4
|
-
import type { BaseController } from './
|
|
4
|
+
import type { BaseController } from './base-controller.js';
|
|
5
5
|
import { Log } from '@spinajs/log';
|
|
6
6
|
export interface IParamDoc {
|
|
7
7
|
name: string;
|
|
@@ -83,8 +83,18 @@ export declare class DefaultControllerCache extends AsyncService {
|
|
|
83
83
|
protected CacheFS: fFs;
|
|
84
84
|
protected Hasher: FileHasher;
|
|
85
85
|
resolve(): Promise<void>;
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Returns parameter-name map used for route argument binding.
|
|
88
|
+
*
|
|
89
|
+
* Cache entries are keyed by source-file content hash, so a changed file
|
|
90
|
+
* naturally gets a fresh entry. `options.rebuild` forces regeneration and
|
|
91
|
+
* overwrite even when entries for the current hash already exist — used by
|
|
92
|
+
* the `http:controllers:cache` CLI command to refresh a pre-built cache
|
|
93
|
+
* (e.g. inside a docker image build).
|
|
94
|
+
*/
|
|
95
|
+
getCache(controller: ClassInfo<BaseController>, options?: {
|
|
96
|
+
rebuild?: boolean;
|
|
97
|
+
}): Promise<Record<string, string[]>>;
|
|
88
98
|
/** Whether `file` points at a real on-disk source we can parse. */
|
|
89
99
|
private isResolvableSource;
|
|
90
100
|
/**
|
|
@@ -160,4 +170,16 @@ export declare class DefaultControllerCache extends AsyncService {
|
|
|
160
170
|
*/
|
|
161
171
|
private schemaFromNamedType;
|
|
162
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether a ClassInfo `file` points at a real on-disk source. Bracketed
|
|
175
|
+
* sentinels (e.g. `<di>`, `<dynamic>`) are used by Controllers for entries
|
|
176
|
+
* that were never loaded from a file.
|
|
177
|
+
*/
|
|
178
|
+
export declare function isOnDiskSource(file: string | undefined): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Pull parameter names from `Function.prototype.toString()` output. Modern V8
|
|
181
|
+
* preserves the literal signature so we can regex out `(a, b = 1, ...rest)`.
|
|
182
|
+
* Stripped of default values and TypeScript type annotations.
|
|
183
|
+
*/
|
|
184
|
+
export declare function parseFnParamNames(fn: Function): string[];
|
|
163
185
|
//# sourceMappingURL=cache.d.ts.map
|
package/lib/cjs/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAI5B,OAAO,EAAE,YAAY,EAAc,SAAS,EAAa,MAAM,aAAa,CAAC;AAC7E,OAAO,EAAE,EAAE,IAAI,GAAG,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"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"}
|
package/lib/cjs/cache.js
CHANGED
|
@@ -13,6 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.DefaultControllerCache = void 0;
|
|
16
|
+
exports.isOnDiskSource = isOnDiskSource;
|
|
17
|
+
exports.parseFnParamNames = parseFnParamNames;
|
|
16
18
|
const typescript_1 = __importDefault(require("typescript"));
|
|
17
19
|
const path_1 = require("path");
|
|
18
20
|
const fs_1 = require("fs");
|
|
@@ -57,8 +59,16 @@ let DefaultControllerCache = class DefaultControllerCache extends di_1.AsyncServ
|
|
|
57
59
|
await super.resolve();
|
|
58
60
|
this.Log.info(`Controller cache dir is: ${this.CacheFS.resolvePath('')}`);
|
|
59
61
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Returns parameter-name map used for route argument binding.
|
|
64
|
+
*
|
|
65
|
+
* Cache entries are keyed by source-file content hash, so a changed file
|
|
66
|
+
* naturally gets a fresh entry. `options.rebuild` forces regeneration and
|
|
67
|
+
* overwrite even when entries for the current hash already exist — used by
|
|
68
|
+
* the `http:controllers:cache` CLI command to refresh a pre-built cache
|
|
69
|
+
* (e.g. inside a docker image build).
|
|
70
|
+
*/
|
|
71
|
+
async getCache(controller, options) {
|
|
62
72
|
// Sentinel values like `<di>` are set by Controllers.resolve() / add() for
|
|
63
73
|
// controllers registered through DI rather than a file scan. There's no
|
|
64
74
|
// on-disk source to parse, so fall back to a runtime extraction of
|
|
@@ -69,8 +79,9 @@ let DefaultControllerCache = class DefaultControllerCache extends di_1.AsyncServ
|
|
|
69
79
|
const file = (0, path_1.resolve)(controller.file.replace('.js', '.d.ts'));
|
|
70
80
|
const hash = await this.Hasher.hash(file);
|
|
71
81
|
const docHash = `doc_${hash}`;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
82
|
+
const rebuild = options?.rebuild === true;
|
|
83
|
+
const paramExists = !rebuild && (await this.CacheFS.exists(hash));
|
|
84
|
+
const docExists = !rebuild && (await this.CacheFS.exists(docHash));
|
|
74
85
|
if (!paramExists || !docExists) {
|
|
75
86
|
this.Log.info(`Generating controller cache for ${controller.name}`);
|
|
76
87
|
const { parameters, documentation } = this.extractAll(file, controller.name);
|
|
@@ -84,11 +95,7 @@ let DefaultControllerCache = class DefaultControllerCache extends di_1.AsyncServ
|
|
|
84
95
|
}
|
|
85
96
|
/** Whether `file` points at a real on-disk source we can parse. */
|
|
86
97
|
isResolvableSource(file) {
|
|
87
|
-
|
|
88
|
-
return false;
|
|
89
|
-
// Bracketed sentinels (e.g. `<di>`, `<dynamic>`) are used by Controllers
|
|
90
|
-
// for entries that were never loaded from a file.
|
|
91
|
-
return !(file.startsWith('<') && file.endsWith('>'));
|
|
98
|
+
return isOnDiskSource(file);
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
94
101
|
* Fallback: derive route method parameter names by parsing each method's
|
|
@@ -543,6 +550,16 @@ __decorate([
|
|
|
543
550
|
exports.DefaultControllerCache = DefaultControllerCache = __decorate([
|
|
544
551
|
(0, di_1.Singleton)()
|
|
545
552
|
], DefaultControllerCache);
|
|
553
|
+
/**
|
|
554
|
+
* Whether a ClassInfo `file` points at a real on-disk source. Bracketed
|
|
555
|
+
* sentinels (e.g. `<di>`, `<dynamic>`) are used by Controllers for entries
|
|
556
|
+
* that were never loaded from a file.
|
|
557
|
+
*/
|
|
558
|
+
function isOnDiskSource(file) {
|
|
559
|
+
if (!file)
|
|
560
|
+
return false;
|
|
561
|
+
return !(file.startsWith('<') && file.endsWith('>'));
|
|
562
|
+
}
|
|
546
563
|
/** Rightmost identifier of an entity name. */
|
|
547
564
|
function entityNameRight(name) {
|
|
548
565
|
return typescript_1.default.isIdentifier(name) ? name.text : name.right.text;
|