@xfcfam/xf-server-http 0.1.0
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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/src/api/A.d.ts +18 -0
- package/dist/src/api/A.d.ts.map +1 -0
- package/dist/src/api/A.js +18 -0
- package/dist/src/api/A.js.map +1 -0
- package/dist/src/api/general/GraphQLService.d.ts +48 -0
- package/dist/src/api/general/GraphQLService.d.ts.map +1 -0
- package/dist/src/api/general/GraphQLService.js +43 -0
- package/dist/src/api/general/GraphQLService.js.map +1 -0
- package/dist/src/api/general/ObjectRestService.d.ts +196 -0
- package/dist/src/api/general/ObjectRestService.d.ts.map +1 -0
- package/dist/src/api/general/ObjectRestService.js +289 -0
- package/dist/src/api/general/ObjectRestService.js.map +1 -0
- package/dist/src/api/general/RestService.d.ts +62 -0
- package/dist/src/api/general/RestService.d.ts.map +1 -0
- package/dist/src/api/general/RestService.js +75 -0
- package/dist/src/api/general/RestService.js.map +1 -0
- package/dist/src/api/general/WebSocketService.d.ts +42 -0
- package/dist/src/api/general/WebSocketService.d.ts.map +1 -0
- package/dist/src/api/general/WebSocketService.js +45 -0
- package/dist/src/api/general/WebSocketService.js.map +1 -0
- package/dist/src/api/utils/FileResponseUtils.d.ts +66 -0
- package/dist/src/api/utils/FileResponseUtils.d.ts.map +1 -0
- package/dist/src/api/utils/FileResponseUtils.js +82 -0
- package/dist/src/api/utils/FileResponseUtils.js.map +1 -0
- package/dist/src/api/utils/HttpStatusUtils.d.ts +36 -0
- package/dist/src/api/utils/HttpStatusUtils.d.ts.map +1 -0
- package/dist/src/api/utils/HttpStatusUtils.js +40 -0
- package/dist/src/api/utils/HttpStatusUtils.js.map +1 -0
- package/dist/src/api/utils/ResponseUtils.d.ts +61 -0
- package/dist/src/api/utils/ResponseUtils.d.ts.map +1 -0
- package/dist/src/api/utils/ResponseUtils.js +81 -0
- package/dist/src/api/utils/ResponseUtils.js.map +1 -0
- package/dist/src/api/utils/SchemaValidatorUtils.d.ts +48 -0
- package/dist/src/api/utils/SchemaValidatorUtils.d.ts.map +1 -0
- package/dist/src/api/utils/SchemaValidatorUtils.js +52 -0
- package/dist/src/api/utils/SchemaValidatorUtils.js.map +1 -0
- package/dist/src/api/utils/SseUtils.d.ts +57 -0
- package/dist/src/api/utils/SseUtils.d.ts.map +1 -0
- package/dist/src/api/utils/SseUtils.js +78 -0
- package/dist/src/api/utils/SseUtils.js.map +1 -0
- package/dist/src/business/B.d.ts +18 -0
- package/dist/src/business/B.d.ts.map +1 -0
- package/dist/src/business/B.js +18 -0
- package/dist/src/business/B.js.map +1 -0
- package/dist/src/business/general/HttpServerBusiness.d.ts +190 -0
- package/dist/src/business/general/HttpServerBusiness.d.ts.map +1 -0
- package/dist/src/business/general/HttpServerBusiness.js +364 -0
- package/dist/src/business/general/HttpServerBusiness.js.map +1 -0
- package/dist/src/business/transfers/BadRequestException.d.ts +13 -0
- package/dist/src/business/transfers/BadRequestException.d.ts.map +1 -0
- package/dist/src/business/transfers/BadRequestException.js +16 -0
- package/dist/src/business/transfers/BadRequestException.js.map +1 -0
- package/dist/src/business/transfers/ForbiddenException.d.ts +13 -0
- package/dist/src/business/transfers/ForbiddenException.d.ts.map +1 -0
- package/dist/src/business/transfers/ForbiddenException.js +16 -0
- package/dist/src/business/transfers/ForbiddenException.js.map +1 -0
- package/dist/src/business/transfers/GraphQLConfig.d.ts +25 -0
- package/dist/src/business/transfers/GraphQLConfig.d.ts.map +1 -0
- package/dist/src/business/transfers/GraphQLConfig.js +2 -0
- package/dist/src/business/transfers/GraphQLConfig.js.map +1 -0
- package/dist/src/business/transfers/HttpException.d.ts +23 -0
- package/dist/src/business/transfers/HttpException.d.ts.map +1 -0
- package/dist/src/business/transfers/HttpException.js +27 -0
- package/dist/src/business/transfers/HttpException.js.map +1 -0
- package/dist/src/business/transfers/HttpMethod.d.ts +7 -0
- package/dist/src/business/transfers/HttpMethod.d.ts.map +1 -0
- package/dist/src/business/transfers/HttpMethod.js +2 -0
- package/dist/src/business/transfers/HttpMethod.js.map +1 -0
- package/dist/src/business/transfers/HttpRequest.d.ts +35 -0
- package/dist/src/business/transfers/HttpRequest.d.ts.map +1 -0
- package/dist/src/business/transfers/HttpRequest.js +2 -0
- package/dist/src/business/transfers/HttpRequest.js.map +1 -0
- package/dist/src/business/transfers/HttpResponse.d.ts +28 -0
- package/dist/src/business/transfers/HttpResponse.d.ts.map +1 -0
- package/dist/src/business/transfers/HttpResponse.js +2 -0
- package/dist/src/business/transfers/HttpResponse.js.map +1 -0
- package/dist/src/business/transfers/InternalServerException.d.ts +13 -0
- package/dist/src/business/transfers/InternalServerException.d.ts.map +1 -0
- package/dist/src/business/transfers/InternalServerException.js +16 -0
- package/dist/src/business/transfers/InternalServerException.js.map +1 -0
- package/dist/src/business/transfers/MultipartPart.d.ts +38 -0
- package/dist/src/business/transfers/MultipartPart.d.ts.map +1 -0
- package/dist/src/business/transfers/MultipartPart.js +2 -0
- package/dist/src/business/transfers/MultipartPart.js.map +1 -0
- package/dist/src/business/transfers/NotFoundException.d.ts +13 -0
- package/dist/src/business/transfers/NotFoundException.d.ts.map +1 -0
- package/dist/src/business/transfers/NotFoundException.js +16 -0
- package/dist/src/business/transfers/NotFoundException.js.map +1 -0
- package/dist/src/business/transfers/Route.d.ts +25 -0
- package/dist/src/business/transfers/Route.d.ts.map +1 -0
- package/dist/src/business/transfers/Route.js +2 -0
- package/dist/src/business/transfers/Route.js.map +1 -0
- package/dist/src/business/transfers/UnauthorizedException.d.ts +13 -0
- package/dist/src/business/transfers/UnauthorizedException.d.ts.map +1 -0
- package/dist/src/business/transfers/UnauthorizedException.js +16 -0
- package/dist/src/business/transfers/UnauthorizedException.js.map +1 -0
- package/dist/src/business/transfers/WebSocketConnection.d.ts +40 -0
- package/dist/src/business/transfers/WebSocketConnection.d.ts.map +1 -0
- package/dist/src/business/transfers/WebSocketConnection.js +2 -0
- package/dist/src/business/transfers/WebSocketConnection.js.map +1 -0
- package/dist/src/repository/R.d.ts +18 -0
- package/dist/src/repository/R.d.ts.map +1 -0
- package/dist/src/repository/R.js +18 -0
- package/dist/src/repository/R.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { RestService } from './RestService.js';
|
|
2
|
+
import type { HttpRequest } from '../../business/transfers/HttpRequest.js';
|
|
3
|
+
import type { HttpResponse } from '../../business/transfers/HttpResponse.js';
|
|
4
|
+
import type { HttpHandler } from '../../business/transfers/Route.js';
|
|
5
|
+
/**
|
|
6
|
+
* Body parser: given the raw bytes of a request and the value of the
|
|
7
|
+
* `Content-Type` header, produce the parsed JS object. May throw on
|
|
8
|
+
* invalid input; the pipeline wraps the failure in a
|
|
9
|
+
* `BadRequestException`.
|
|
10
|
+
*/
|
|
11
|
+
export type BodyParser = (bytes: Uint8Array, contentType: string) => unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Body serializer: given a JS value and the negotiated media type,
|
|
14
|
+
* produce the byte buffer to send on the wire. Mirror of
|
|
15
|
+
* {@link BodyParser}.
|
|
16
|
+
*/
|
|
17
|
+
export type BodySerializer = (value: unknown, mediaType: string) => Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* Configuration accepted by {@link ObjectRestService}'s constructor.
|
|
20
|
+
* All keys are optional — sensible JSON defaults are wired
|
|
21
|
+
* automatically.
|
|
22
|
+
*/
|
|
23
|
+
export interface ObjectRestOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Content-Type → parser map. Keys are matched case-insensitively
|
|
26
|
+
* against the request's `Content-Type` (the media type before any
|
|
27
|
+
* `;` parameters). User entries override the built-in JSON parser.
|
|
28
|
+
*
|
|
29
|
+
* To support XML / CSV / YAML, supply the parser yourself and let
|
|
30
|
+
* the consumer choose the underlying library:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* new MyService({
|
|
34
|
+
* parsers: new Map([
|
|
35
|
+
* ['application/xml', (bytes) => xmlParser(decode(bytes))],
|
|
36
|
+
* ]),
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
readonly parsers?: ReadonlyMap<string, BodyParser>;
|
|
41
|
+
/**
|
|
42
|
+
* Media-type → serializer map. Selected by the response's
|
|
43
|
+
* `Content-Type` (defaulting to `application/json`).
|
|
44
|
+
*/
|
|
45
|
+
readonly serializers?: ReadonlyMap<string, BodySerializer>;
|
|
46
|
+
/**
|
|
47
|
+
* Media type to use for serialisation when the response does not
|
|
48
|
+
* carry an explicit `Content-Type`. Default `'application/json'`.
|
|
49
|
+
*/
|
|
50
|
+
readonly defaultMediaType?: string;
|
|
51
|
+
/**
|
|
52
|
+
* When `true`, the built-in JSON parser revives ISO-8601 date strings
|
|
53
|
+
* into `Date` objects (via {@link ObjectRestService.dateReviver}), so
|
|
54
|
+
* handlers receive real `Date`s on the request side. Serialising
|
|
55
|
+
* `Date`s back to string is automatic — see {@link formatDate} — so
|
|
56
|
+
* dates round-trip with this single flag. Default `false`.
|
|
57
|
+
*/
|
|
58
|
+
readonly reviveDates?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* How a `Date` is rendered to text when serialising the response body.
|
|
61
|
+
* The developer decides the wire format: the default is ISO-8601
|
|
62
|
+
* (`date.toISOString()`), but any function works — date-only, epoch
|
|
63
|
+
* millis, a locale string, or a custom formatter.
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* new MyService({ formatDate: (d) => d.toISOString().slice(0, 10) }) // YYYY-MM-DD
|
|
67
|
+
* new MyService({ formatDate: (d) => String(d.getTime()) }) // epoch ms
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Applied to every `Date` found anywhere in the response body (nested
|
|
71
|
+
* objects and arrays included). Default: ISO-8601.
|
|
72
|
+
*/
|
|
73
|
+
readonly formatDate?: (date: Date) => string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Generalization for Interaction Layer components that operate on
|
|
77
|
+
* **parsed JS objects** on both sides of the wire — the typical REST
|
|
78
|
+
* service.
|
|
79
|
+
*
|
|
80
|
+
* Extends {@link RestService} with automatic body parsing and
|
|
81
|
+
* serialisation. Use `this.object(handler)` when registering a route
|
|
82
|
+
* to wrap the handler in the full parse → handle → serialize pipeline:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Combine with `this.wrap(this.object(handler))` to add the
|
|
89
|
+
* per-service `onRequest` / `onResponse` hooks on top.
|
|
90
|
+
*
|
|
91
|
+
* **Built-in**: JSON parse + serialize, with developer-chosen date
|
|
92
|
+
* handling (see {@link ObjectRestService.dateReviver}, the `reviveDates`
|
|
93
|
+
* option, and `formatDate` in {@link ObjectRestOptions}). XML/CSV/YAML
|
|
94
|
+
* parsing and any other serializers are wired by passing them in
|
|
95
|
+
* {@link ObjectRestOptions}, keeping the package zero-dependency.
|
|
96
|
+
*
|
|
97
|
+
* Stream bodies (request or response) bypass parsing/serialisation:
|
|
98
|
+
* if the request body is a `ReadableStream` the handler may consume
|
|
99
|
+
* it directly; if the handler returns a `ReadableStream` or a
|
|
100
|
+
* `Uint8Array` it is sent verbatim.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { ObjectRestService, HttpStatusUtils } from '@xfcfam/xf-server'
|
|
105
|
+
* import { B } from '../../business/B.js'
|
|
106
|
+
*
|
|
107
|
+
* export class UsersRestService extends ObjectRestService {
|
|
108
|
+
* override async init(): Promise<void> {
|
|
109
|
+
* B.server.get('/users/:id', this.wrap(this.getUser))
|
|
110
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* private async getUser(req: HttpRequest): Promise<HttpResponse> {
|
|
114
|
+
* const user = await B.usersBusiness.findById(req.params.id!)
|
|
115
|
+
* return { status: HttpStatusUtils.OK, body: user }
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* private async createUser(req: HttpRequest): Promise<HttpResponse> {
|
|
119
|
+
* const body = req.body as { name: string; email: string }
|
|
120
|
+
* const user = await B.usersBusiness.create(body)
|
|
121
|
+
* return { status: HttpStatusUtils.CREATED, body: user }
|
|
122
|
+
* }
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare abstract class ObjectRestService extends RestService {
|
|
127
|
+
protected readonly parsers: ReadonlyMap<string, BodyParser>;
|
|
128
|
+
protected readonly serializers: ReadonlyMap<string, BodySerializer>;
|
|
129
|
+
protected readonly defaultMediaType: string;
|
|
130
|
+
/** Developer-chosen `Date` → text formatter for the JSON serializer. */
|
|
131
|
+
protected readonly formatDate: (date: Date) => string;
|
|
132
|
+
constructor(options?: ObjectRestOptions);
|
|
133
|
+
/**
|
|
134
|
+
* Wrap a handler in the body parse → handle → serialize pipeline,
|
|
135
|
+
* plus the per-service `onRequest` / `onResponse` / `onError` hooks.
|
|
136
|
+
*
|
|
137
|
+
* This is the recommended registration method for
|
|
138
|
+
* `ObjectRestService` handlers:
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* The pipeline runs:
|
|
145
|
+
* 1. `onRequest(req)` (per-service hook)
|
|
146
|
+
* 2. parse request body (bytes → object, by Content-Type)
|
|
147
|
+
* 3. handler
|
|
148
|
+
* 4. `onResponse(req, res)` (per-service hook)
|
|
149
|
+
* 5. serialize response body (object → bytes, by Content-Type)
|
|
150
|
+
*
|
|
151
|
+
* `onResponse` fires before serialisation so envelope wrappers see
|
|
152
|
+
* the original payload.
|
|
153
|
+
*
|
|
154
|
+
* Use `this.wrap(handler)` instead when the handler already manages
|
|
155
|
+
* its own bytes (upload / download / SSE endpoints).
|
|
156
|
+
*/
|
|
157
|
+
protected object<TReqBody = unknown, TResBody = unknown>(handler: HttpHandler<TReqBody, TResBody>): HttpHandler;
|
|
158
|
+
/**
|
|
159
|
+
* Decode the request body to a JS value using the registered
|
|
160
|
+
* parser for `Content-Type`. Stream bodies are buffered first.
|
|
161
|
+
* Exposed as `protected` so subclasses can call it directly when
|
|
162
|
+
* composing custom pipelines.
|
|
163
|
+
*/
|
|
164
|
+
protected parseRequestBody(request: HttpRequest): Promise<HttpRequest>;
|
|
165
|
+
/**
|
|
166
|
+
* Encode the response body to bytes using the registered serializer
|
|
167
|
+
* for the response's `Content-Type` (or the default media type).
|
|
168
|
+
* Streams and Uint8Arrays are passed through unchanged. Exposed as
|
|
169
|
+
* `protected` so subclasses can call it directly when composing
|
|
170
|
+
* custom pipelines.
|
|
171
|
+
*/
|
|
172
|
+
protected serializeResponseBody(response: HttpResponse): HttpResponse;
|
|
173
|
+
private static parseJson;
|
|
174
|
+
private static serializeJson;
|
|
175
|
+
private static toIsoString;
|
|
176
|
+
/**
|
|
177
|
+
* Deep-copy `value`, replacing every `Date` with `formatDate(date)` so
|
|
178
|
+
* a developer-chosen date format survives `JSON.stringify` (which would
|
|
179
|
+
* otherwise call `Date.toJSON` and force ISO-8601).
|
|
180
|
+
*/
|
|
181
|
+
private static applyDateFormat;
|
|
182
|
+
/**
|
|
183
|
+
* JSON reviver that turns ISO-8601 date strings into `Date` objects —
|
|
184
|
+
* the developer shortcut for date handling. Use it directly with
|
|
185
|
+
* `JSON.parse(text, ObjectRestService.dateReviver)`, or set
|
|
186
|
+
* `reviveDates: true` in {@link ObjectRestOptions} to apply it to the
|
|
187
|
+
* built-in JSON parser. The inverse is automatic: a `Date` serializes
|
|
188
|
+
* back to text via the `formatDate` option (ISO-8601 by default).
|
|
189
|
+
*/
|
|
190
|
+
static dateReviver(_key: string, value: unknown): unknown;
|
|
191
|
+
private static readonly ISO_DATE_RE;
|
|
192
|
+
private static mediaTypeOf;
|
|
193
|
+
private static isReadableStream;
|
|
194
|
+
private static collect;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=ObjectRestService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectRestService.d.ts","sourceRoot":"","sources":["../../../../src/api/general/ObjectRestService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAA;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAA;AAGpE;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAA;AAE5E;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,UAAU,CAAA;AAE9E;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAClD;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC1D;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAA;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,8BAAsB,iBAAkB,SAAQ,WAAW;IACzD,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC3D,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IACnE,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IAC3C,wEAAwE;IACxE,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAA;gBAEzC,OAAO,GAAE,iBAAsB;IA2B3C;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,CAAC,MAAM,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO,EACrD,OAAO,EAAE,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,GACvC,WAAW;IAiBd;;;;;OAKG;cACa,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAgC5E;;;;;;OAMG;IACH,SAAS,CAAC,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,YAAY;IA6BrE,OAAO,CAAC,MAAM,CAAC,SAAS;IAKxB,OAAO,CAAC,MAAM,CAAC,aAAa;IAS5B,OAAO,CAAC,MAAM,CAAC,WAAW;IAI1B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAe9B;;;;;;;OAOG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;IAQzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CACqC;IAExE,OAAO,CAAC,MAAM,CAAC,WAAW;IAO1B,OAAO,CAAC,MAAM,CAAC,gBAAgB;mBAMV,OAAO;CAc7B"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { RestService } from './RestService.js';
|
|
2
|
+
import { BadRequestException } from '../../business/transfers/BadRequestException.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generalization for Interaction Layer components that operate on
|
|
5
|
+
* **parsed JS objects** on both sides of the wire — the typical REST
|
|
6
|
+
* service.
|
|
7
|
+
*
|
|
8
|
+
* Extends {@link RestService} with automatic body parsing and
|
|
9
|
+
* serialisation. Use `this.object(handler)` when registering a route
|
|
10
|
+
* to wrap the handler in the full parse → handle → serialize pipeline:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Combine with `this.wrap(this.object(handler))` to add the
|
|
17
|
+
* per-service `onRequest` / `onResponse` hooks on top.
|
|
18
|
+
*
|
|
19
|
+
* **Built-in**: JSON parse + serialize, with developer-chosen date
|
|
20
|
+
* handling (see {@link ObjectRestService.dateReviver}, the `reviveDates`
|
|
21
|
+
* option, and `formatDate` in {@link ObjectRestOptions}). XML/CSV/YAML
|
|
22
|
+
* parsing and any other serializers are wired by passing them in
|
|
23
|
+
* {@link ObjectRestOptions}, keeping the package zero-dependency.
|
|
24
|
+
*
|
|
25
|
+
* Stream bodies (request or response) bypass parsing/serialisation:
|
|
26
|
+
* if the request body is a `ReadableStream` the handler may consume
|
|
27
|
+
* it directly; if the handler returns a `ReadableStream` or a
|
|
28
|
+
* `Uint8Array` it is sent verbatim.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { ObjectRestService, HttpStatusUtils } from '@xfcfam/xf-server'
|
|
33
|
+
* import { B } from '../../business/B.js'
|
|
34
|
+
*
|
|
35
|
+
* export class UsersRestService extends ObjectRestService {
|
|
36
|
+
* override async init(): Promise<void> {
|
|
37
|
+
* B.server.get('/users/:id', this.wrap(this.getUser))
|
|
38
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* private async getUser(req: HttpRequest): Promise<HttpResponse> {
|
|
42
|
+
* const user = await B.usersBusiness.findById(req.params.id!)
|
|
43
|
+
* return { status: HttpStatusUtils.OK, body: user }
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* private async createUser(req: HttpRequest): Promise<HttpResponse> {
|
|
47
|
+
* const body = req.body as { name: string; email: string }
|
|
48
|
+
* const user = await B.usersBusiness.create(body)
|
|
49
|
+
* return { status: HttpStatusUtils.CREATED, body: user }
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export class ObjectRestService extends RestService {
|
|
55
|
+
parsers;
|
|
56
|
+
serializers;
|
|
57
|
+
defaultMediaType;
|
|
58
|
+
/** Developer-chosen `Date` → text formatter for the JSON serializer. */
|
|
59
|
+
formatDate;
|
|
60
|
+
constructor(options = {}) {
|
|
61
|
+
super();
|
|
62
|
+
this.formatDate = options.formatDate ?? ObjectRestService.toIsoString;
|
|
63
|
+
const jsonParser = options.reviveDates === true
|
|
64
|
+
? (bytes) => JSON.parse(new TextDecoder('utf-8').decode(bytes), ObjectRestService.dateReviver)
|
|
65
|
+
: ObjectRestService.parseJson;
|
|
66
|
+
const parsers = new Map([
|
|
67
|
+
['application/json', jsonParser],
|
|
68
|
+
]);
|
|
69
|
+
if (options.parsers !== undefined) {
|
|
70
|
+
for (const [k, v] of options.parsers)
|
|
71
|
+
parsers.set(k.toLowerCase(), v);
|
|
72
|
+
}
|
|
73
|
+
this.parsers = parsers;
|
|
74
|
+
const formatDate = this.formatDate;
|
|
75
|
+
const serializers = new Map([
|
|
76
|
+
['application/json', (value) => ObjectRestService.serializeJson(value, formatDate)],
|
|
77
|
+
]);
|
|
78
|
+
if (options.serializers !== undefined) {
|
|
79
|
+
for (const [k, v] of options.serializers)
|
|
80
|
+
serializers.set(k.toLowerCase(), v);
|
|
81
|
+
}
|
|
82
|
+
this.serializers = serializers;
|
|
83
|
+
this.defaultMediaType = options.defaultMediaType ?? 'application/json';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Wrap a handler in the body parse → handle → serialize pipeline,
|
|
87
|
+
* plus the per-service `onRequest` / `onResponse` / `onError` hooks.
|
|
88
|
+
*
|
|
89
|
+
* This is the recommended registration method for
|
|
90
|
+
* `ObjectRestService` handlers:
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* B.server.post('/users', this.object(this.createUser))
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* The pipeline runs:
|
|
97
|
+
* 1. `onRequest(req)` (per-service hook)
|
|
98
|
+
* 2. parse request body (bytes → object, by Content-Type)
|
|
99
|
+
* 3. handler
|
|
100
|
+
* 4. `onResponse(req, res)` (per-service hook)
|
|
101
|
+
* 5. serialize response body (object → bytes, by Content-Type)
|
|
102
|
+
*
|
|
103
|
+
* `onResponse` fires before serialisation so envelope wrappers see
|
|
104
|
+
* the original payload.
|
|
105
|
+
*
|
|
106
|
+
* Use `this.wrap(handler)` instead when the handler already manages
|
|
107
|
+
* its own bytes (upload / download / SSE endpoints).
|
|
108
|
+
*/
|
|
109
|
+
object(handler) {
|
|
110
|
+
const bound = handler.bind(this);
|
|
111
|
+
return async (req) => {
|
|
112
|
+
let r = await this.onRequest(req);
|
|
113
|
+
try {
|
|
114
|
+
r = await this.parseRequestBody(r);
|
|
115
|
+
const res = await bound(r);
|
|
116
|
+
const afterHook = await this.onResponse(r, res);
|
|
117
|
+
return this.serializeResponseBody(afterHook);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const resolved = await this.onError(r, err);
|
|
121
|
+
if (resolved !== undefined)
|
|
122
|
+
return resolved;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Decode the request body to a JS value using the registered
|
|
129
|
+
* parser for `Content-Type`. Stream bodies are buffered first.
|
|
130
|
+
* Exposed as `protected` so subclasses can call it directly when
|
|
131
|
+
* composing custom pipelines.
|
|
132
|
+
*/
|
|
133
|
+
async parseRequestBody(request) {
|
|
134
|
+
const body = request.body;
|
|
135
|
+
if (body === null || body === undefined)
|
|
136
|
+
return request;
|
|
137
|
+
// Multipart bodies are already an array of MultipartPart (the
|
|
138
|
+
// server adapter produced them before the parser ran). Pass
|
|
139
|
+
// through unchanged.
|
|
140
|
+
if (Array.isArray(body))
|
|
141
|
+
return request;
|
|
142
|
+
// Pass through non-bytes (e.g. already-parsed, or handler chose to consume the stream directly).
|
|
143
|
+
if (!(body instanceof Uint8Array) && !ObjectRestService.isReadableStream(body))
|
|
144
|
+
return request;
|
|
145
|
+
const bytes = body instanceof Uint8Array
|
|
146
|
+
? body
|
|
147
|
+
: await ObjectRestService.collect(body);
|
|
148
|
+
if (bytes.byteLength === 0)
|
|
149
|
+
return { ...request, body: null };
|
|
150
|
+
const contentType = ObjectRestService.mediaTypeOf(request.headers['content-type']);
|
|
151
|
+
const parser = this.parsers.get(contentType.toLowerCase());
|
|
152
|
+
if (parser === undefined) {
|
|
153
|
+
// Unknown content-type → leave the bytes as-is; the handler may
|
|
154
|
+
// know what to do with them. Common case for binary uploads
|
|
155
|
+
// that bypass parsing.
|
|
156
|
+
return { ...request, body: bytes };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const parsed = parser(bytes, contentType);
|
|
160
|
+
return { ...request, body: parsed };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
throw new BadRequestException(`Invalid ${contentType} body`, { detail: err.message });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Encode the response body to bytes using the registered serializer
|
|
168
|
+
* for the response's `Content-Type` (or the default media type).
|
|
169
|
+
* Streams and Uint8Arrays are passed through unchanged. Exposed as
|
|
170
|
+
* `protected` so subclasses can call it directly when composing
|
|
171
|
+
* custom pipelines.
|
|
172
|
+
*/
|
|
173
|
+
serializeResponseBody(response) {
|
|
174
|
+
const body = response.body;
|
|
175
|
+
if (body === null || body === undefined)
|
|
176
|
+
return response;
|
|
177
|
+
if (body instanceof Uint8Array)
|
|
178
|
+
return response;
|
|
179
|
+
if (typeof body === 'string')
|
|
180
|
+
return response;
|
|
181
|
+
if (ObjectRestService.isReadableStream(body))
|
|
182
|
+
return response;
|
|
183
|
+
const headers = response.headers ?? {};
|
|
184
|
+
const explicit = headers['content-type'];
|
|
185
|
+
const mediaType = ObjectRestService.mediaTypeOf(explicit) || this.defaultMediaType;
|
|
186
|
+
const serializer = this.serializers.get(mediaType.toLowerCase());
|
|
187
|
+
if (serializer === undefined) {
|
|
188
|
+
// Fall back to JSON serialisation if the explicit media type
|
|
189
|
+
// is unknown — a sensible default rather than a 500.
|
|
190
|
+
return {
|
|
191
|
+
status: response.status,
|
|
192
|
+
headers: { ...headers, 'content-type': this.defaultMediaType },
|
|
193
|
+
body: ObjectRestService.serializeJson(body, this.formatDate),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
status: response.status,
|
|
198
|
+
headers: { ...headers, 'content-type': mediaType },
|
|
199
|
+
body: serializer(body, mediaType),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
203
|
+
static parseJson(bytes, _ct) {
|
|
204
|
+
const text = new TextDecoder('utf-8').decode(bytes);
|
|
205
|
+
return JSON.parse(text);
|
|
206
|
+
}
|
|
207
|
+
static serializeJson(value, formatDate) {
|
|
208
|
+
// Fast path: the default ISO formatter matches `Date.toJSON`, so
|
|
209
|
+
// `JSON.stringify` already produces the right text — skip the walk.
|
|
210
|
+
const prepared = formatDate === ObjectRestService.toIsoString
|
|
211
|
+
? value
|
|
212
|
+
: ObjectRestService.applyDateFormat(value, formatDate);
|
|
213
|
+
return new TextEncoder().encode(JSON.stringify(prepared));
|
|
214
|
+
}
|
|
215
|
+
static toIsoString(date) {
|
|
216
|
+
return date.toISOString();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Deep-copy `value`, replacing every `Date` with `formatDate(date)` so
|
|
220
|
+
* a developer-chosen date format survives `JSON.stringify` (which would
|
|
221
|
+
* otherwise call `Date.toJSON` and force ISO-8601).
|
|
222
|
+
*/
|
|
223
|
+
static applyDateFormat(value, formatDate) {
|
|
224
|
+
if (value instanceof Date)
|
|
225
|
+
return formatDate(value);
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
return value.map(v => ObjectRestService.applyDateFormat(v, formatDate));
|
|
228
|
+
}
|
|
229
|
+
if (value !== null && typeof value === 'object') {
|
|
230
|
+
const out = {};
|
|
231
|
+
for (const [k, v] of Object.entries(value)) {
|
|
232
|
+
out[k] = ObjectRestService.applyDateFormat(v, formatDate);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* JSON reviver that turns ISO-8601 date strings into `Date` objects —
|
|
240
|
+
* the developer shortcut for date handling. Use it directly with
|
|
241
|
+
* `JSON.parse(text, ObjectRestService.dateReviver)`, or set
|
|
242
|
+
* `reviveDates: true` in {@link ObjectRestOptions} to apply it to the
|
|
243
|
+
* built-in JSON parser. The inverse is automatic: a `Date` serializes
|
|
244
|
+
* back to text via the `formatDate` option (ISO-8601 by default).
|
|
245
|
+
*/
|
|
246
|
+
static dateReviver(_key, value) {
|
|
247
|
+
if (typeof value === 'string' && ObjectRestService.ISO_DATE_RE.test(value)) {
|
|
248
|
+
const ms = Date.parse(value);
|
|
249
|
+
if (!Number.isNaN(ms))
|
|
250
|
+
return new Date(ms);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
static ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
255
|
+
static mediaTypeOf(contentType) {
|
|
256
|
+
const ct = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
257
|
+
if (typeof ct !== 'string' || ct.length === 0)
|
|
258
|
+
return '';
|
|
259
|
+
const semi = ct.indexOf(';');
|
|
260
|
+
return (semi < 0 ? ct : ct.substring(0, semi)).trim().toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
static isReadableStream(value) {
|
|
263
|
+
return typeof value === 'object'
|
|
264
|
+
&& value !== null
|
|
265
|
+
&& typeof value.getReader === 'function';
|
|
266
|
+
}
|
|
267
|
+
static async collect(stream) {
|
|
268
|
+
const reader = stream.getReader();
|
|
269
|
+
const chunks = [];
|
|
270
|
+
let total = 0;
|
|
271
|
+
while (true) {
|
|
272
|
+
const { done, value } = await reader.read();
|
|
273
|
+
if (done)
|
|
274
|
+
break;
|
|
275
|
+
if (value !== undefined) {
|
|
276
|
+
chunks.push(value);
|
|
277
|
+
total += value.byteLength;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const out = new Uint8Array(total);
|
|
281
|
+
let offset = 0;
|
|
282
|
+
for (const chunk of chunks) {
|
|
283
|
+
out.set(chunk, offset);
|
|
284
|
+
offset += chunk.byteLength;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=ObjectRestService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectRestService.js","sourceRoot":"","sources":["../../../../src/api/general/ObjectRestService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EntryService } from '@xfcfam/xf-server';
|
|
2
|
+
import type { HttpRequest } from '../../business/transfers/HttpRequest.js';
|
|
3
|
+
import type { HttpResponse } from '../../business/transfers/HttpResponse.js';
|
|
4
|
+
/**
|
|
5
|
+
* Interaction-Layer Generalization for components that expose a set of
|
|
6
|
+
* HTTP endpoints over the Business-Layer server.
|
|
7
|
+
*
|
|
8
|
+
* The HTTP specialisation of {@link EntryService} (from
|
|
9
|
+
* `@xfcfam/xf-server`): it fixes the request/response types to
|
|
10
|
+
* `HttpRequest` / `HttpResponse` and adds HTTP response builders. The
|
|
11
|
+
* per-service pipeline (`wrap`, `onRequest` / `onResponse` / `onError`)
|
|
12
|
+
* is inherited unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Concrete subclasses register their endpoints by calling
|
|
15
|
+
* `B.server.get(path, this.wrap(handler))` (or `post`, `put`, …) from
|
|
16
|
+
* within `init()`. The server (`HttpServerBusiness`) holds the route
|
|
17
|
+
* registry and starts Fastify; `RestService` contributes handlers via
|
|
18
|
+
* the push registration API.
|
|
19
|
+
*
|
|
20
|
+
* Body handling is **raw**: the request body arrives either as a
|
|
21
|
+
* `Uint8Array` (when fully buffered) or as a `ReadableStream<Uint8Array>`
|
|
22
|
+
* (when streamed). For the common case of JSON REST APIs that work with
|
|
23
|
+
* parsed objects on both sides, extend {@link ObjectRestService} — it
|
|
24
|
+
* adds automatic content-negotiation parsing and serialisation.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { RestService, HttpStatusUtils } from '@xfcfam/xf-server-http'
|
|
29
|
+
* import { B } from '../../business/B.js'
|
|
30
|
+
*
|
|
31
|
+
* export class HealthRestService extends RestService {
|
|
32
|
+
* override async init(): Promise<void> {
|
|
33
|
+
* B.server.get('/health', this.wrap(this.health))
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* private async health(_req: HttpRequest): Promise<HttpResponse> {
|
|
37
|
+
* return { status: HttpStatusUtils.OK, body: { status: 'ok' } }
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare abstract class RestService extends EntryService<HttpRequest, HttpResponse> {
|
|
43
|
+
/**
|
|
44
|
+
* Convenience: build a successful response. Default status 200.
|
|
45
|
+
*/
|
|
46
|
+
protected ok<T>(body: T, headers?: Record<string, string>): HttpResponse<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Convenience: build a failure response. Default status 500.
|
|
49
|
+
*/
|
|
50
|
+
protected ko<T>(body: T, status?: number, headers?: Record<string, string>): HttpResponse<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Convenience: build a no-body 204. Use after successful mutations
|
|
53
|
+
* that don't return a payload.
|
|
54
|
+
*/
|
|
55
|
+
protected empty(headers?: Record<string, string>): HttpResponse;
|
|
56
|
+
/**
|
|
57
|
+
* Convenience: throw an `InternalServerException`. Use only when no
|
|
58
|
+
* more specific HttpException applies.
|
|
59
|
+
*/
|
|
60
|
+
protected fail(message: string, body?: unknown): never;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=RestService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RestService.d.ts","sourceRoot":"","sources":["../../../../src/api/general/RestService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAA;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAA;AAG5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,8BAAsB,WAAY,SAAQ,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC;IAC/E;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC;IAO3E;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,SAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC;IAOzF;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY;IAI/D;;;OAGG;IACH,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK;CAGvD"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EntryService } from '@xfcfam/xf-server';
|
|
2
|
+
import { InternalServerException } from '../../business/transfers/InternalServerException.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interaction-Layer Generalization for components that expose a set of
|
|
5
|
+
* HTTP endpoints over the Business-Layer server.
|
|
6
|
+
*
|
|
7
|
+
* The HTTP specialisation of {@link EntryService} (from
|
|
8
|
+
* `@xfcfam/xf-server`): it fixes the request/response types to
|
|
9
|
+
* `HttpRequest` / `HttpResponse` and adds HTTP response builders. The
|
|
10
|
+
* per-service pipeline (`wrap`, `onRequest` / `onResponse` / `onError`)
|
|
11
|
+
* is inherited unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Concrete subclasses register their endpoints by calling
|
|
14
|
+
* `B.server.get(path, this.wrap(handler))` (or `post`, `put`, …) from
|
|
15
|
+
* within `init()`. The server (`HttpServerBusiness`) holds the route
|
|
16
|
+
* registry and starts Fastify; `RestService` contributes handlers via
|
|
17
|
+
* the push registration API.
|
|
18
|
+
*
|
|
19
|
+
* Body handling is **raw**: the request body arrives either as a
|
|
20
|
+
* `Uint8Array` (when fully buffered) or as a `ReadableStream<Uint8Array>`
|
|
21
|
+
* (when streamed). For the common case of JSON REST APIs that work with
|
|
22
|
+
* parsed objects on both sides, extend {@link ObjectRestService} — it
|
|
23
|
+
* adds automatic content-negotiation parsing and serialisation.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { RestService, HttpStatusUtils } from '@xfcfam/xf-server-http'
|
|
28
|
+
* import { B } from '../../business/B.js'
|
|
29
|
+
*
|
|
30
|
+
* export class HealthRestService extends RestService {
|
|
31
|
+
* override async init(): Promise<void> {
|
|
32
|
+
* B.server.get('/health', this.wrap(this.health))
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* private async health(_req: HttpRequest): Promise<HttpResponse> {
|
|
36
|
+
* return { status: HttpStatusUtils.OK, body: { status: 'ok' } }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class RestService extends EntryService {
|
|
42
|
+
/**
|
|
43
|
+
* Convenience: build a successful response. Default status 200.
|
|
44
|
+
*/
|
|
45
|
+
ok(body, headers) {
|
|
46
|
+
const out = headers === undefined
|
|
47
|
+
? { status: 200, body }
|
|
48
|
+
: { status: 200, body, headers };
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Convenience: build a failure response. Default status 500.
|
|
53
|
+
*/
|
|
54
|
+
ko(body, status = 500, headers) {
|
|
55
|
+
const out = headers === undefined
|
|
56
|
+
? { status, body }
|
|
57
|
+
: { status, body, headers };
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Convenience: build a no-body 204. Use after successful mutations
|
|
62
|
+
* that don't return a payload.
|
|
63
|
+
*/
|
|
64
|
+
empty(headers) {
|
|
65
|
+
return headers === undefined ? { status: 204 } : { status: 204, headers };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convenience: throw an `InternalServerException`. Use only when no
|
|
69
|
+
* more specific HttpException applies.
|
|
70
|
+
*/
|
|
71
|
+
fail(message, body) {
|
|
72
|
+
throw new InternalServerException(message, body);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=RestService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RestService.js","sourceRoot":"","sources":["../../../../src/api/general/RestService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qDAAqD,CAAA;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,OAAgB,WAAY,SAAQ,YAAuC;IAC/E;;OAEG;IACO,EAAE,CAAI,IAAO,EAAE,OAAgC;QACvD,MAAM,GAAG,GAAoB,OAAO,KAAK,SAAS;YAChD,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE;YACvB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;QAClC,OAAO,GAAG,CAAA;IACZ,CAAC;IAED;;OAEG;IACO,EAAE,CAAI,IAAO,EAAE,MAAM,GAAG,GAAG,EAAE,OAAgC;QACrE,MAAM,GAAG,GAAoB,OAAO,KAAK,SAAS;YAChD,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE;YAClB,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;QAC7B,OAAO,GAAG,CAAA;IACZ,CAAC;IAED;;;OAGG;IACO,KAAK,CAAC,OAAgC;QAC9C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAA;IAC3E,CAAC;IAED;;;OAGG;IACO,IAAI,CAAC,OAAe,EAAE,IAAc;QAC5C,MAAM,IAAI,uBAAuB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAClD,CAAC;CACF"}
|