@ololoepepe/controllers 0.3.0 → 0.4.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `JsonResponse` takes the body as a type parameter constrained to `Json` instead of
|
|
15
|
+
`unknown`, and `jsonController` gained a matching third parameter, inferred from the
|
|
16
|
+
callback. The body of a JSON response was typed `unknown`, so `json(undefined)`
|
|
17
|
+
type-checked even though `undefined` is not a JSON value. Also rejected are `bigint`
|
|
18
|
+
(`JSON.stringify` throws on it), `symbol` and functions, nested fields included.
|
|
19
|
+
|
|
20
|
+
A callback that returns nothing still sends the empty object, which is what
|
|
21
|
+
`EmptyJsonBody` names, so a response has to admit it alongside its own body type —
|
|
22
|
+
`JsonResponse<EmptyJsonBody | Tenant>`. `null` is unaffected: it is a JSON value and
|
|
23
|
+
goes out as it is.
|
|
24
|
+
|
|
25
|
+
Response bodies now have to be declared with `type` rather than `interface`:
|
|
26
|
+
TypeScript assigns no `interface` to a type with an index signature, which `Json`
|
|
27
|
+
rests on.
|
|
28
|
+
|
|
29
|
+
Breaking for code that names `JsonResponse` explicitly; the type argument is now
|
|
30
|
+
required. Code that only passes callbacks is unaffected.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Exported types `Json` and `EmptyJsonBody`.
|
|
35
|
+
|
|
10
36
|
## [0.3.0] - 2026-08-28
|
|
11
37
|
|
|
12
38
|
### Changed
|
package/README.md
CHANGED
|
@@ -149,17 +149,59 @@ The request and the response are type parameters, defaulting to Express's
|
|
|
149
149
|
your own shapes — the controllers only require what they actually use:
|
|
150
150
|
|
|
151
151
|
```ts
|
|
152
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
jsonController,
|
|
154
|
+
type ControllerRequest,
|
|
155
|
+
type EmptyJsonBody,
|
|
156
|
+
type JsonResponse
|
|
157
|
+
} from '@ololoepepe/controllers';
|
|
158
|
+
|
|
159
|
+
type Tenant = {
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
};
|
|
153
163
|
|
|
154
164
|
interface TenantRequest extends ControllerRequest {
|
|
155
165
|
tenantId: string;
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
const controller = jsonController<TenantRequest, JsonResponse
|
|
168
|
+
const controller = jsonController<TenantRequest, JsonResponse<EmptyJsonBody | Tenant>, Tenant>(
|
|
169
|
+
request => loadTenant(request.tenantId)
|
|
170
|
+
);
|
|
159
171
|
```
|
|
160
172
|
|
|
173
|
+
`jsonController` has a third parameter for the body, inferred from the callback,
|
|
174
|
+
so you only name it when you name the response as well. It is constrained to
|
|
175
|
+
`Json` — what JSON can actually hold:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
export type Json = boolean | Json[] | null | number | string | {[key: string]: Json};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
That rules out `undefined`, `bigint` (`JSON.stringify` throws on it), `symbol`
|
|
182
|
+
and functions, and it checks nested fields too.
|
|
183
|
+
|
|
184
|
+
**Describe response bodies with `type`, not `interface`.** TypeScript does not
|
|
185
|
+
assign an `interface` to a type with an index signature, so an interface will not
|
|
186
|
+
satisfy `Json` however JSON-shaped it is — including one nested inside a `type`:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
interface Tenant {id: string} // will not compile as a body
|
|
190
|
+
type Tenant = {id: string}; // will
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`Date` does not satisfy `Json` either. Say `string`, which is what a date is by
|
|
194
|
+
the time it reaches the client anyway — `JSON.stringify` calls `toJSON` on it.
|
|
195
|
+
Optional properties are fine: `type Tenant = {id: string; note?: string}` passes.
|
|
196
|
+
|
|
197
|
+
The callback may still return `undefined`; that is the documented "no body" case,
|
|
198
|
+
and what goes out instead is the empty object `EmptyJsonBody`. This is why the
|
|
199
|
+
response above has to accept `EmptyJsonBody | Tenant` rather than just `Tenant`.
|
|
200
|
+
`null` is left alone, being a JSON value like any other.
|
|
201
|
+
|
|
161
202
|
Exported types: `Controller`, `ControllerCallback`, `ControllerRequest`,
|
|
162
|
-
`JsonResponse`, `NextFunction`, `NoContentResponse`,
|
|
203
|
+
`EmptyJsonBody`, `Json`, `JsonResponse`, `NextFunction`, `NoContentResponse`,
|
|
204
|
+
`StringResponse`.
|
|
163
205
|
|
|
164
206
|
## Development
|
|
165
207
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Request, Response } from 'express';
|
|
2
|
-
import type { Controller, ControllerCallback, ControllerRequest, JsonResponse } from '#src/types.ts';
|
|
2
|
+
import type { Controller, ControllerCallback, ControllerRequest, EmptyJsonBody, Json, JsonResponse } from '#src/types.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Создает контроллер, который вызывает `callback` на каждый запрос и отправляет его результат
|
|
5
5
|
* в виде JSON.
|
|
@@ -14,4 +14,4 @@ import type { Controller, ControllerCallback, ControllerRequest, JsonResponse }
|
|
|
14
14
|
* @param callback - функция, вызываемая на входящий запрос
|
|
15
15
|
* @param statusCode - код ответа, если нужен не тот, что выбирается по методу запроса
|
|
16
16
|
*/
|
|
17
|
-
export default function jsonController<TRequest extends ControllerRequest = Request, TResponse extends JsonResponse = Response>(callback: ControllerCallback<TRequest, TResponse>, statusCode?: number): Controller<TRequest, TResponse>;
|
|
17
|
+
export default function jsonController<TRequest extends ControllerRequest = Request, TResponse extends JsonResponse<EmptyJsonBody | TBody> = Response, TBody extends Json = EmptyJsonBody>(callback: ControllerCallback<TRequest, TResponse, TBody | undefined>, statusCode?: number): Controller<TRequest, TResponse>;
|
|
@@ -17,7 +17,10 @@ export default function jsonController(callback, statusCode) {
|
|
|
17
17
|
return async (request, response, next) => {
|
|
18
18
|
try {
|
|
19
19
|
const result = await callback(request, response);
|
|
20
|
-
//
|
|
20
|
+
// Подменяется только `undefined`: значения `undefined` в JSON нет, а `null` — осмысленное
|
|
21
|
+
// тело ответа. Правило считает эту тернарку равнозначной `??`, потому что не видит `null`
|
|
22
|
+
// внутри свободного параметра `TBody`, но `??` подменил бы и его.
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `??` подменил бы и `null`
|
|
21
24
|
const body = (result === undefined) ? {} : result;
|
|
22
25
|
response.status(statusCode ?? selectStatusCode(request.method)).json(body);
|
|
23
26
|
}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { jsonController, noContentController, stringController } from '#src/controllers/index.ts';
|
|
2
|
-
export type { Controller, ControllerCallback, ControllerRequest, JsonResponse, NextFunction, NoContentResponse, StringResponse } from '#src/types.ts';
|
|
2
|
+
export type { Controller, ControllerCallback, ControllerRequest, EmptyJsonBody, Json, JsonResponse, NextFunction, NoContentResponse, StringResponse } from '#src/types.ts';
|
package/dist/node/types.d.ts
CHANGED
|
@@ -11,10 +11,28 @@ export type NextFunction = (error?: unknown) => void;
|
|
|
11
11
|
export interface ControllerRequest {
|
|
12
12
|
method: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Значение, которое можно отдать в JSON.
|
|
16
|
+
*
|
|
17
|
+
* Того, чего в JSON нет, здесь и не перечислено: `undefined`, `bigint` (на нем `JSON.stringify`
|
|
18
|
+
* падает), `symbol` и функция телом ответа стать не смогут.
|
|
19
|
+
*
|
|
20
|
+
* У объектов проверяются и поля, вглубь. Цена этого в том, что TypeScript не присваивает
|
|
21
|
+
* `interface` типу с индексной сигнатурой, поэтому тело ответа описывается через `type`, а не
|
|
22
|
+
* через `interface`.
|
|
23
|
+
*/
|
|
24
|
+
export type Json = boolean | Json[] | null | number | string | {
|
|
25
|
+
[key: string]: Json;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Тело, которое контроллер отправляет вместо `undefined`: значения `undefined` в JSON нет,
|
|
29
|
+
* поэтому вместо него уходит пустой объект.
|
|
30
|
+
*/
|
|
31
|
+
export type EmptyJsonBody = Record<string, never>;
|
|
14
32
|
/** Ответ, умеющий отдать тело в виде JSON. */
|
|
15
|
-
export interface JsonResponse {
|
|
33
|
+
export interface JsonResponse<TBody extends Json> {
|
|
16
34
|
status: (statusCode: number) => {
|
|
17
|
-
json: (body:
|
|
35
|
+
json: (body: TBody) => unknown;
|
|
18
36
|
};
|
|
19
37
|
}
|
|
20
38
|
/** Ответ, умеющий отдать тело в виде строки. */
|