@gusnips/server 0.1.1 → 0.2.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/README.md +25 -0
- package/dist/hono/index.d.ts +6 -0
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/index.js +6 -0
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/responses.d.ts +43 -0
- package/dist/hono/responses.d.ts.map +1 -0
- package/dist/hono/responses.js +20 -0
- package/dist/hono/responses.js.map +1 -0
- package/dist/logger/serialize.d.ts +9 -2
- package/dist/logger/serialize.d.ts.map +1 -1
- package/dist/logger/serialize.js +12 -3
- package/dist/logger/serialize.js.map +1 -1
- package/dist/responses.d.ts +14 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +14 -1
- package/dist/responses.js.map +1 -1
- package/package.json +1 -1
- package/src/hono/index.ts +6 -0
- package/src/hono/responses.test.ts +50 -0
- package/src/hono/responses.ts +41 -0
- package/src/logger/serialize.test.ts +25 -0
- package/src/logger/serialize.ts +12 -3
- package/src/responses.ts +14 -1
package/README.md
CHANGED
|
@@ -42,6 +42,18 @@ paginated(rows, { total: 128, limit: 20, offset: 100 });
|
|
|
42
42
|
`hasMore` is computed from the rows you actually returned, not from `limit`, so a page cut short
|
|
43
43
|
by a filter still answers honestly.
|
|
44
44
|
|
|
45
|
+
**What comes back is an answer, not a body.** A framework wants the `body`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
return c.json(ok(data), 200); // WRONG: {"status":200,"body":{"data":…}}
|
|
49
|
+
return c.json(ok(data).body, 200); // the envelope
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Nothing catches the first line. `c.json` takes any JSON value, so the types hold; the status is
|
|
53
|
+
still 200, so a health probe and a deploy gate both pass; and a test that calls `ok` never sees
|
|
54
|
+
the body its caller sends. That shipped, and a client found it fifty minutes later. On Hono,
|
|
55
|
+
import the four adapters from `@gusnips/server/hono` and the question does not arise.
|
|
56
|
+
|
|
45
57
|
## Refusing a request
|
|
46
58
|
|
|
47
59
|
Declare your own codes and what status each one answers with. The `satisfies` is the line that
|
|
@@ -256,6 +268,19 @@ app.onError(errorHandler({ errorResponse, logger }));
|
|
|
256
268
|
app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
|
|
257
269
|
```
|
|
258
270
|
|
|
271
|
+
```ts
|
|
272
|
+
import { created, noContent, ok, paginated } from "@gusnips/server/hono";
|
|
273
|
+
|
|
274
|
+
app.get("/users/:id", (c) => ok(c, user)); // { data: user }, 200
|
|
275
|
+
app.post("/users", (c) => created(c, user)); // 201
|
|
276
|
+
app.get("/users", (c) => paginated(c, rows, { total, limit, offset }));
|
|
277
|
+
app.delete("/users/:id", (c) => noContent(c)); // 204, no body, no content-type
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Three adopters wrote those four functions by hand before they were here, and one of the three
|
|
281
|
+
got the unwrap wrong in production. `ok` takes an explicit status for the cases that are not
|
|
282
|
+
200 — `ok(c, job, 202)` where the route accepted rather than answered.
|
|
283
|
+
|
|
259
284
|
**`errorBoundary` is not optional.** Hono hands `onError` only what is `instanceof Error`.
|
|
260
285
|
Anything else is rethrown past every layer and escapes as an unhandled rejection: no answer, a
|
|
261
286
|
dropped connection, and a browser that reports it as a CORS failure — which sends whoever reads
|
package/dist/hono/index.d.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* app.onError(errorHandler({ errorResponse, logger }));
|
|
7
7
|
* app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
|
|
8
8
|
*
|
|
9
|
+
* `ok`, `created`, `paginated` and `noContent` are the success half: they take the `Context` and
|
|
10
|
+
* put the envelope on the wire, so no route has to unwrap the `{ status, body }` answer that the
|
|
11
|
+
* framework-free builders return. Three adopters wrote them by hand and one got that unwrap wrong
|
|
12
|
+
* in production — see `responses.ts` beside this file.
|
|
13
|
+
*
|
|
9
14
|
* and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
|
|
10
15
|
* app itself uses for what anyone may call.
|
|
11
16
|
*/
|
|
@@ -13,6 +18,7 @@ export { errorBoundary, errorHandler, notFoundHandler } from "./errors.ts";
|
|
|
13
18
|
export type { ErrorHandlerOptions } from "./errors.ts";
|
|
14
19
|
export { assertEveryRouteGuarded, guard, underAny } from "./guards.ts";
|
|
15
20
|
export type { GuardCheckOptions } from "./guards.ts";
|
|
21
|
+
export { created, noContent, ok, paginated } from "./responses.ts";
|
|
16
22
|
export { requestLogger } from "./request-logger.ts";
|
|
17
23
|
export type { RequestLoggerOptions, RequestVariables } from "./request-logger.ts";
|
|
18
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/hono/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/hono/index.js
CHANGED
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
* app.onError(errorHandler({ errorResponse, logger }));
|
|
7
7
|
* app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
|
|
8
8
|
*
|
|
9
|
+
* `ok`, `created`, `paginated` and `noContent` are the success half: they take the `Context` and
|
|
10
|
+
* put the envelope on the wire, so no route has to unwrap the `{ status, body }` answer that the
|
|
11
|
+
* framework-free builders return. Three adopters wrote them by hand and one got that unwrap wrong
|
|
12
|
+
* in production — see `responses.ts` beside this file.
|
|
13
|
+
*
|
|
9
14
|
* and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
|
|
10
15
|
* app itself uses for what anyone may call.
|
|
11
16
|
*/
|
|
12
17
|
export { errorBoundary, errorHandler, notFoundHandler } from "./errors.js";
|
|
13
18
|
export { assertEveryRouteGuarded, guard, underAny } from "./guards.js";
|
|
19
|
+
export { created, noContent, ok, paginated } from "./responses.js";
|
|
14
20
|
export { requestLogger } from "./request-logger.js";
|
|
15
21
|
//# sourceMappingURL=index.js.map
|
package/dist/hono/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The success half of the Hono edge: four adapters that put the envelope on the wire.
|
|
3
|
+
*
|
|
4
|
+
* They exist because the builders in `../responses.ts` return an ANSWER — `{ status, body }` —
|
|
5
|
+
* and every Hono adopter therefore has to unwrap one before `c.json` sees it. Three of three
|
|
6
|
+
* wrote these same four functions by hand, and one of the three wrote `c.json(ok(data))` instead
|
|
7
|
+
* of `c.json(ok(data).body)`: every 200 from a live API answered
|
|
8
|
+
* `{"status":200,"body":{"data":…}}` for fifty minutes. Nothing caught it. `c.json` takes any
|
|
9
|
+
* JSON value, so the types were satisfied; the status was still 200, so every probe and every
|
|
10
|
+
* deploy gate was satisfied; and a test that calls the module never sees the body its caller
|
|
11
|
+
* sends. A client found it, because a client is the only reader that parses the envelope.
|
|
12
|
+
*
|
|
13
|
+
* A doc comment would have been read by whoever was already careful. These make the wrong line
|
|
14
|
+
* unreachable, which is the only fix available to a package that owns both sides of the seam.
|
|
15
|
+
*/
|
|
16
|
+
import type { PaginationMeta } from "@gusnips/http";
|
|
17
|
+
import type { Context } from "hono";
|
|
18
|
+
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
19
|
+
/** `return ok(c, user)` — or `ok(c, job, 202)` where the route accepted rather than answered. */
|
|
20
|
+
export declare function ok<T>(c: Context, data: T, status?: ContentfulStatusCode): Response & import("hono").TypedResponse<PaginationMeta | T | undefined extends bigint | readonly bigint[] ? never : { [K in keyof {
|
|
21
|
+
data: T;
|
|
22
|
+
meta?: PaginationMeta | undefined;
|
|
23
|
+
} as (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_1 ? T_1 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_1 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) extends true ? never : K]: boolean extends (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_2 ? T_2 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_2 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) ? import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]> | undefined : import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]>; }, 429 | 500 | 200 | 201 | 400 | 401 | 100 | 102 | 103 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 431 | 451 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">;
|
|
24
|
+
export declare function created<T>(c: Context, data: T): Response & import("hono").TypedResponse<PaginationMeta | T | undefined extends bigint | readonly bigint[] ? never : { [K in keyof {
|
|
25
|
+
data: T;
|
|
26
|
+
meta?: PaginationMeta | undefined;
|
|
27
|
+
} as (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_1 ? T_1 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_1 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) extends true ? never : K]: boolean extends (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_2 ? T_2 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_2 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) ? import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]> | undefined : import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]>; }, 429 | 500 | 200 | 201 | 400 | 401 | 100 | 102 | 103 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 431 | 451 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">;
|
|
28
|
+
/** `hasMore` is computed from the rows actually returned — see `paginated` in `../responses.ts`. */
|
|
29
|
+
export declare function paginated<T>(c: Context, rows: T[], meta: Omit<PaginationMeta, "hasMore">): Response & import("hono").TypedResponse<{
|
|
30
|
+
data: import("hono/utils/types").JSONParsed<T extends import("hono/utils/types").InvalidJSONValue ? null : T, bigint | readonly bigint[]>[];
|
|
31
|
+
meta?: {
|
|
32
|
+
total: number;
|
|
33
|
+
limit: number;
|
|
34
|
+
offset: number;
|
|
35
|
+
hasMore: boolean;
|
|
36
|
+
} | undefined;
|
|
37
|
+
}, 200, "json">;
|
|
38
|
+
/**
|
|
39
|
+
* `c.body(null, 204)`, not `c.json`: a 204 carries no body, and `c.json(null, 204)` writes the
|
|
40
|
+
* four bytes `null` and a `content-type` header under a status that promises neither.
|
|
41
|
+
*/
|
|
42
|
+
export declare function noContent(c: Context): Response & import("hono").TypedResponse<null, 204, "body">;
|
|
43
|
+
//# sourceMappingURL=responses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/hono/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,iGAAiG;AACjG,wBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,GAAE,oBAA0B;;;ooCAE5E;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;;;ooCAE7C;AAED,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;;;;;;;;gBAExF;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,8DAEnC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ok as okBody, paginated as paginatedBody } from "../responses.js";
|
|
2
|
+
/** `return ok(c, user)` — or `ok(c, job, 202)` where the route accepted rather than answered. */
|
|
3
|
+
export function ok(c, data, status = 200) {
|
|
4
|
+
return c.json(okBody(data).body, status);
|
|
5
|
+
}
|
|
6
|
+
export function created(c, data) {
|
|
7
|
+
return ok(c, data, 201);
|
|
8
|
+
}
|
|
9
|
+
/** `hasMore` is computed from the rows actually returned — see `paginated` in `../responses.ts`. */
|
|
10
|
+
export function paginated(c, rows, meta) {
|
|
11
|
+
return c.json(paginatedBody(rows, meta).body, 200);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `c.body(null, 204)`, not `c.json`: a 204 carries no body, and `c.json(null, 204)` writes the
|
|
15
|
+
* four bytes `null` and a `content-type` header under a status that promises neither.
|
|
16
|
+
*/
|
|
17
|
+
export function noContent(c) {
|
|
18
|
+
return c.body(null, 204);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=responses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/hono/responses.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE3E,iGAAiG;AACjG,MAAM,UAAU,EAAE,CAAI,CAAU,EAAE,IAAO,EAAE,SAA+B,GAAG;IAC3E,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,OAAO,CAAI,CAAU,EAAE,IAAO;IAC5C,OAAO,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,SAAS,CAAI,CAAU,EAAE,IAAS,EAAE,IAAqC;IACvF,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,CAAU;IAClC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -20,8 +20,15 @@
|
|
|
20
20
|
* A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
|
|
21
21
|
* a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
|
|
22
22
|
* the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
|
|
23
|
-
* any other key stays untouched. `name` and `
|
|
24
|
-
* carries them and a line with
|
|
23
|
+
* any other key stays untouched. `name`, `message` and `stack` come along because a rejection
|
|
24
|
+
* object usually carries them and a line with none of them says nothing at all.
|
|
25
|
+
*
|
|
26
|
+
* `stack` is here because an adopter's queue found it missing. A job that dies is stored by its
|
|
27
|
+
* queue through a serializer, so the error reaching the dead-letter handler is a plain object with
|
|
28
|
+
* its stack in a string — and that stack is the whole of the "why" in a line whose job is to say
|
|
29
|
+
* which job died and why. The Error branch below has always written `stack` unfiltered; leaving it
|
|
30
|
+
* out here was an asymmetry, not a decision. It is a conventional field name, not one an SDK hangs
|
|
31
|
+
* its own inputs off, which is what the allow-list exists to stop.
|
|
25
32
|
*
|
|
26
33
|
* Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
|
|
27
34
|
* logger's `meta`, which is untouched — `cause` is not the place for it, and one incident of a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2EH
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2EH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEtE;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,aAAa,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAiCvF;AAED,qGAAqG;AACrG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC"}
|
package/dist/logger/serialize.js
CHANGED
|
@@ -90,8 +90,15 @@ function keptValue(key, value) {
|
|
|
90
90
|
* A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
|
|
91
91
|
* a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
|
|
92
92
|
* the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
|
|
93
|
-
* any other key stays untouched. `name` and `
|
|
94
|
-
* carries them and a line with
|
|
93
|
+
* any other key stays untouched. `name`, `message` and `stack` come along because a rejection
|
|
94
|
+
* object usually carries them and a line with none of them says nothing at all.
|
|
95
|
+
*
|
|
96
|
+
* `stack` is here because an adopter's queue found it missing. A job that dies is stored by its
|
|
97
|
+
* queue through a serializer, so the error reaching the dead-letter handler is a plain object with
|
|
98
|
+
* its stack in a string — and that stack is the whole of the "why" in a line whose job is to say
|
|
99
|
+
* which job died and why. The Error branch below has always written `stack` unfiltered; leaving it
|
|
100
|
+
* out here was an asymmetry, not a decision. It is a conventional field name, not one an SDK hangs
|
|
101
|
+
* its own inputs off, which is what the allow-list exists to stop.
|
|
95
102
|
*
|
|
96
103
|
* Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
|
|
97
104
|
* logger's `meta`, which is untouched — `cause` is not the place for it, and one incident of a
|
|
@@ -103,11 +110,13 @@ export function narrowErrorLike(value) {
|
|
|
103
110
|
function narrow(value, seen) {
|
|
104
111
|
seen.add(value);
|
|
105
112
|
const out = {};
|
|
106
|
-
const { name, message, cause } = value;
|
|
113
|
+
const { name, message, stack, cause } = value;
|
|
107
114
|
if (typeof name === "string")
|
|
108
115
|
out.name = name;
|
|
109
116
|
if (typeof message === "string")
|
|
110
117
|
out.message = message;
|
|
118
|
+
if (typeof stack === "string")
|
|
119
|
+
out.stack = stack;
|
|
111
120
|
for (const [k, v] of Object.entries(value)) {
|
|
112
121
|
if (KEPT_ERROR_FIELDS.has(k))
|
|
113
122
|
out[k] = keptValue(k, v);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IACrD,sFAAsF;IACtF,yFAAyF;IACzF,4FAA4F;IAC5F,MAAM;IACN,QAAQ;IACR,SAAS;IACT,MAAM;IACN,YAAY;IACZ,UAAU;IACV,wEAAwE;IACxE,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,gBAAgB;IAChB,8FAA8F;IAC9F,+FAA+F;IAC/F,iEAAiE;IACjE,YAAY;IACZ,MAAM;IACN,WAAW;IACX,6FAA6F;IAC7F,+EAA+E;IAC/E,MAAM;CACP,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAChD,MAAM,WAAW,GAAG,wEAAwE,CAAC;AAE7F,SAAS,SAAS,CAAC,GAAW,EAAE,KAAc;IAC5C,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,SAAS,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"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"}
|
package/dist/responses.d.ts
CHANGED
|
@@ -11,7 +11,20 @@
|
|
|
11
11
|
* callers. The framework adapter is eight lines and lives in `/hono`.
|
|
12
12
|
*/
|
|
13
13
|
import type { ApiError, ApiSuccess, PaginationMeta } from "@gusnips/http";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The `body` is `{ data }`, or `{ data, meta }` where a route has counts to report.
|
|
16
|
+
*
|
|
17
|
+
* What comes back is an ANSWER — `{ status, body }` — not a body, because this layer is
|
|
18
|
+
* framework-free and has to hand its caller a status too. An adapter takes `.body`:
|
|
19
|
+
*
|
|
20
|
+
* return c.json(ok(data), status); // WRONG: {"status":200,"body":{"data":…}}
|
|
21
|
+
* return c.json(ok(data).body, status); // the envelope
|
|
22
|
+
*
|
|
23
|
+
* Nothing catches the first line — `c.json` takes any JSON value, the status is still whatever
|
|
24
|
+
* you passed, and a test that calls this module never sees the body its caller sends. It shipped,
|
|
25
|
+
* and a client found it. On Hono, import `ok` from `@gusnips/server/hono` instead: the four
|
|
26
|
+
* adapters there take the `Context`, and the question does not arise.
|
|
27
|
+
*/
|
|
15
28
|
export declare function ok<T, M = PaginationMeta>(data: T, meta?: M): {
|
|
16
29
|
status: 200;
|
|
17
30
|
body: ApiSuccess<T, M>;
|
package/dist/responses.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1E
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;;;EAG1D;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;;;EAGjC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;;;EAM5E;AAED,wBAAgB,SAAS;;;EAExB;AAED,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrB,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;CAC1C;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAClE,IAAI,EAAE,IAAI,CAAC;IACX,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAC3E,sDAAsD;IACtD,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACjC,oCAAoC;IACpC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IAC9B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuDD;;;;GAIG;AACH,UAAU,QAAQ;IAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC,GAAG,eAAe,EAAE,CAUpB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,MAAM,GAAG,MAAM,EAC3F,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,IAIP,KAAK,OAAO,KAAG,WAAW,CAAC,IAAI,CAAC,CA8C/D"}
|
package/dist/responses.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { AppError } from "./errors.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The `body` is `{ data }`, or `{ data, meta }` where a route has counts to report.
|
|
4
|
+
*
|
|
5
|
+
* What comes back is an ANSWER — `{ status, body }` — not a body, because this layer is
|
|
6
|
+
* framework-free and has to hand its caller a status too. An adapter takes `.body`:
|
|
7
|
+
*
|
|
8
|
+
* return c.json(ok(data), status); // WRONG: {"status":200,"body":{"data":…}}
|
|
9
|
+
* return c.json(ok(data).body, status); // the envelope
|
|
10
|
+
*
|
|
11
|
+
* Nothing catches the first line — `c.json` takes any JSON value, the status is still whatever
|
|
12
|
+
* you passed, and a test that calls this module never sees the body its caller sends. It shipped,
|
|
13
|
+
* and a client found it. On Hono, import `ok` from `@gusnips/server/hono` instead: the four
|
|
14
|
+
* adapters there take the `Context`, and the question does not arise.
|
|
15
|
+
*/
|
|
3
16
|
export function ok(data, meta) {
|
|
4
17
|
const body = meta === undefined ? { data } : { data, meta };
|
|
5
18
|
return { status: 200, body };
|
package/dist/responses.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC
|
|
1
|
+
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/hono/index.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* app.onError(errorHandler({ errorResponse, logger }));
|
|
7
7
|
* app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
|
|
8
8
|
*
|
|
9
|
+
* `ok`, `created`, `paginated` and `noContent` are the success half: they take the `Context` and
|
|
10
|
+
* put the envelope on the wire, so no route has to unwrap the `{ status, body }` answer that the
|
|
11
|
+
* framework-free builders return. Three adopters wrote them by hand and one got that unwrap wrong
|
|
12
|
+
* in production — see `responses.ts` beside this file.
|
|
13
|
+
*
|
|
9
14
|
* and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
|
|
10
15
|
* app itself uses for what anyone may call.
|
|
11
16
|
*/
|
|
@@ -13,5 +18,6 @@ export { errorBoundary, errorHandler, notFoundHandler } from "./errors.ts";
|
|
|
13
18
|
export type { ErrorHandlerOptions } from "./errors.ts";
|
|
14
19
|
export { assertEveryRouteGuarded, guard, underAny } from "./guards.ts";
|
|
15
20
|
export type { GuardCheckOptions } from "./guards.ts";
|
|
21
|
+
export { created, noContent, ok, paginated } from "./responses.ts";
|
|
16
22
|
export { requestLogger } from "./request-logger.ts";
|
|
17
23
|
export type { RequestLoggerOptions, RequestVariables } from "./request-logger.ts";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { created, noContent, ok, paginated } from "./responses.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every assertion here reads the BYTES off a real request, never the return value of a builder.
|
|
7
|
+
* That is the point: the outage this file exists to prevent was invisible to any test that calls
|
|
8
|
+
* the module, because the wrong value was still a perfectly good JSON value.
|
|
9
|
+
*/
|
|
10
|
+
const app = new Hono()
|
|
11
|
+
.get("/ok", (c) => ok(c, { id: "a" }))
|
|
12
|
+
.get("/accepted", (c) => ok(c, { id: "a" }, 202))
|
|
13
|
+
.post("/created", (c) => created(c, { id: "a" }))
|
|
14
|
+
.get("/page", (c) => paginated(c, [1, 2], { total: 9, limit: 2, offset: 0 }))
|
|
15
|
+
.get("/last-page", (c) => paginated(c, [9], { total: 9, limit: 2, offset: 8 }))
|
|
16
|
+
.delete("/gone", (c) => noContent(c));
|
|
17
|
+
|
|
18
|
+
describe("the Hono success adapters", () => {
|
|
19
|
+
it("answers the envelope itself, never the { status, body } wrapper around it", async () => {
|
|
20
|
+
const res = await app.request("/ok");
|
|
21
|
+
expect(res.status).toBe(200);
|
|
22
|
+
expect(await res.json()).toEqual({ data: { id: "a" } });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("keeps the status the route asked for", async () => {
|
|
26
|
+
expect((await app.request("/accepted")).status).toBe(202);
|
|
27
|
+
expect((await app.request("/created", { method: "POST" })).status).toBe(201);
|
|
28
|
+
expect(await (await app.request("/created", { method: "POST" })).json()).toEqual({
|
|
29
|
+
data: { id: "a" },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("computes hasMore from the rows returned, not from the limit", async () => {
|
|
34
|
+
expect(await (await app.request("/page")).json()).toEqual({
|
|
35
|
+
data: [1, 2],
|
|
36
|
+
meta: { total: 9, limit: 2, offset: 0, hasMore: true },
|
|
37
|
+
});
|
|
38
|
+
expect(await (await app.request("/last-page")).json()).toEqual({
|
|
39
|
+
data: [9],
|
|
40
|
+
meta: { total: 9, limit: 2, offset: 8, hasMore: false },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("sends a 204 with no body and no content-type", async () => {
|
|
45
|
+
const res = await app.request("/gone", { method: "DELETE" });
|
|
46
|
+
expect(res.status).toBe(204);
|
|
47
|
+
expect(res.headers.get("content-type")).toBeNull();
|
|
48
|
+
expect(await res.text()).toBe("");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The success half of the Hono edge: four adapters that put the envelope on the wire.
|
|
3
|
+
*
|
|
4
|
+
* They exist because the builders in `../responses.ts` return an ANSWER — `{ status, body }` —
|
|
5
|
+
* and every Hono adopter therefore has to unwrap one before `c.json` sees it. Three of three
|
|
6
|
+
* wrote these same four functions by hand, and one of the three wrote `c.json(ok(data))` instead
|
|
7
|
+
* of `c.json(ok(data).body)`: every 200 from a live API answered
|
|
8
|
+
* `{"status":200,"body":{"data":…}}` for fifty minutes. Nothing caught it. `c.json` takes any
|
|
9
|
+
* JSON value, so the types were satisfied; the status was still 200, so every probe and every
|
|
10
|
+
* deploy gate was satisfied; and a test that calls the module never sees the body its caller
|
|
11
|
+
* sends. A client found it, because a client is the only reader that parses the envelope.
|
|
12
|
+
*
|
|
13
|
+
* A doc comment would have been read by whoever was already careful. These make the wrong line
|
|
14
|
+
* unreachable, which is the only fix available to a package that owns both sides of the seam.
|
|
15
|
+
*/
|
|
16
|
+
import type { PaginationMeta } from "@gusnips/http";
|
|
17
|
+
import type { Context } from "hono";
|
|
18
|
+
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
19
|
+
import { ok as okBody, paginated as paginatedBody } from "../responses.ts";
|
|
20
|
+
|
|
21
|
+
/** `return ok(c, user)` — or `ok(c, job, 202)` where the route accepted rather than answered. */
|
|
22
|
+
export function ok<T>(c: Context, data: T, status: ContentfulStatusCode = 200) {
|
|
23
|
+
return c.json(okBody(data).body, status);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function created<T>(c: Context, data: T) {
|
|
27
|
+
return ok(c, data, 201);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `hasMore` is computed from the rows actually returned — see `paginated` in `../responses.ts`. */
|
|
31
|
+
export function paginated<T>(c: Context, rows: T[], meta: Omit<PaginationMeta, "hasMore">) {
|
|
32
|
+
return c.json(paginatedBody(rows, meta).body, 200);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `c.body(null, 204)`, not `c.json`: a 204 carries no body, and `c.json(null, 204)` writes the
|
|
37
|
+
* four bytes `null` and a `content-type` header under a status that promises neither.
|
|
38
|
+
*/
|
|
39
|
+
export function noContent(c: Context) {
|
|
40
|
+
return c.body(null, 204);
|
|
41
|
+
}
|
|
@@ -221,6 +221,31 @@ describe("what an Error contributes to a log line", () => {
|
|
|
221
221
|
expect(JSON.stringify(line)).not.toContain("4242");
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
it("keeps the stack of an error that crossed a queue and arrived as a plain object", () => {
|
|
225
|
+
// A job queue stores a failed job's error through a serializer, so the dead-letter handler is
|
|
226
|
+
// handed a plain object — and the stack is the whole of the "why" in a line whose job is to
|
|
227
|
+
// say which job died and why. Everything the SDK hung beside it still goes.
|
|
228
|
+
const stored = {
|
|
229
|
+
name: "HttpError",
|
|
230
|
+
message: "Not Found",
|
|
231
|
+
stack: "HttpError: Not Found\n at workers.ts:1:1",
|
|
232
|
+
status: 404,
|
|
233
|
+
request: { method: "GET", body: '{"query":"private"}' },
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const line = JSON.parse(JSON.stringify({ error: stored }, errorReplacer())) as Record<
|
|
237
|
+
string,
|
|
238
|
+
Record<string, unknown>
|
|
239
|
+
>;
|
|
240
|
+
|
|
241
|
+
expect(line.error).toEqual({
|
|
242
|
+
name: "HttpError",
|
|
243
|
+
message: "Not Found",
|
|
244
|
+
stack: "HttpError: Not Found\n at workers.ts:1:1",
|
|
245
|
+
status: 404,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
224
249
|
it("allow-lists a thrown plain object passed directly as the error", () => {
|
|
225
250
|
// Hono wraps a non-Error throw as a cause, but workers, fire-and-forget catches and a database
|
|
226
251
|
// client's `{ code, message, details }` rejection reach the logger directly. `error` is the
|
package/src/logger/serialize.ts
CHANGED
|
@@ -94,8 +94,15 @@ function keptValue(key: string, value: unknown): unknown {
|
|
|
94
94
|
* A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
|
|
95
95
|
* a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
|
|
96
96
|
* the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
|
|
97
|
-
* any other key stays untouched. `name` and `
|
|
98
|
-
* carries them and a line with
|
|
97
|
+
* any other key stays untouched. `name`, `message` and `stack` come along because a rejection
|
|
98
|
+
* object usually carries them and a line with none of them says nothing at all.
|
|
99
|
+
*
|
|
100
|
+
* `stack` is here because an adopter's queue found it missing. A job that dies is stored by its
|
|
101
|
+
* queue through a serializer, so the error reaching the dead-letter handler is a plain object with
|
|
102
|
+
* its stack in a string — and that stack is the whole of the "why" in a line whose job is to say
|
|
103
|
+
* which job died and why. The Error branch below has always written `stack` unfiltered; leaving it
|
|
104
|
+
* out here was an asymmetry, not a decision. It is a conventional field name, not one an SDK hangs
|
|
105
|
+
* its own inputs off, which is what the allow-list exists to stop.
|
|
99
106
|
*
|
|
100
107
|
* Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
|
|
101
108
|
* logger's `meta`, which is untouched — `cause` is not the place for it, and one incident of a
|
|
@@ -108,13 +115,15 @@ export function narrowErrorLike(value: object): Record<string, unknown> {
|
|
|
108
115
|
function narrow(value: object, seen: WeakSet<object>): Record<string, unknown> {
|
|
109
116
|
seen.add(value);
|
|
110
117
|
const out: Record<string, unknown> = {};
|
|
111
|
-
const { name, message, cause } = value as {
|
|
118
|
+
const { name, message, stack, cause } = value as {
|
|
112
119
|
name?: unknown;
|
|
113
120
|
message?: unknown;
|
|
121
|
+
stack?: unknown;
|
|
114
122
|
cause?: unknown;
|
|
115
123
|
};
|
|
116
124
|
if (typeof name === "string") out.name = name;
|
|
117
125
|
if (typeof message === "string") out.message = message;
|
|
126
|
+
if (typeof stack === "string") out.stack = stack;
|
|
118
127
|
for (const [k, v] of Object.entries(value)) {
|
|
119
128
|
if (KEPT_ERROR_FIELDS.has(k)) out[k] = keptValue(k, v);
|
|
120
129
|
}
|
package/src/responses.ts
CHANGED
|
@@ -13,7 +13,20 @@
|
|
|
13
13
|
import type { ApiError, ApiSuccess, PaginationMeta } from "@gusnips/http";
|
|
14
14
|
import { AppError } from "./errors.ts";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The `body` is `{ data }`, or `{ data, meta }` where a route has counts to report.
|
|
18
|
+
*
|
|
19
|
+
* What comes back is an ANSWER — `{ status, body }` — not a body, because this layer is
|
|
20
|
+
* framework-free and has to hand its caller a status too. An adapter takes `.body`:
|
|
21
|
+
*
|
|
22
|
+
* return c.json(ok(data), status); // WRONG: {"status":200,"body":{"data":…}}
|
|
23
|
+
* return c.json(ok(data).body, status); // the envelope
|
|
24
|
+
*
|
|
25
|
+
* Nothing catches the first line — `c.json` takes any JSON value, the status is still whatever
|
|
26
|
+
* you passed, and a test that calls this module never sees the body its caller sends. It shipped,
|
|
27
|
+
* and a client found it. On Hono, import `ok` from `@gusnips/server/hono` instead: the four
|
|
28
|
+
* adapters there take the `Context`, and the question does not arise.
|
|
29
|
+
*/
|
|
17
30
|
export function ok<T, M = PaginationMeta>(data: T, meta?: M) {
|
|
18
31
|
const body: ApiSuccess<T, M> = meta === undefined ? { data } : { data, meta };
|
|
19
32
|
return { status: 200 as const, body };
|