@c9up/ream 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -9
- package/dist/HttpKernel.js +2 -2
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +3 -3
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +50 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bootstrap/hyper-server.js.map +1 -1
- package/dist/helpers/compose.d.ts +30 -0
- package/dist/helpers/compose.d.ts.map +1 -0
- package/dist/helpers/compose.js +27 -0
- package/dist/helpers/compose.js.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/is.d.ts +69 -16
- package/dist/helpers/is.d.ts.map +1 -1
- package/dist/helpers/is.js +80 -20
- package/dist/helpers/is.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +9 -3
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +10 -4
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/rpc/RpcRouter.js +2 -2
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.d.ts.map +1 -1
- package/dist/scheduler/Scheduler.js +12 -4
- package/dist/scheduler/Scheduler.js.map +1 -1
- package/dist/services/app.d.ts +6 -0
- package/dist/services/app.d.ts.map +1 -1
- package/dist/services/app.js +8 -0
- package/dist/services/app.js.map +1 -1
- package/dist/services/config.d.ts +15 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +18 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/encryption.d.ts +23 -0
- package/dist/services/encryption.d.ts.map +1 -0
- package/dist/services/encryption.js +33 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/logger.d.ts +29 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +40 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/services/urlBuilder.d.ts +21 -0
- package/dist/services/urlBuilder.d.ts.map +1 -0
- package/dist/services/urlBuilder.js +34 -0
- package/dist/services/urlBuilder.js.map +1 -0
- package/dist/session/Session.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts +44 -44
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +48 -48
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +39 -39
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +49 -49
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +23 -8
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +52 -11
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/testing/testUtils.d.ts +62 -0
- package/dist/testing/testUtils.d.ts.map +1 -0
- package/dist/testing/testUtils.js +77 -0
- package/dist/testing/testUtils.js.map +1 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +21 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/HttpKernel.ts +2 -2
- package/src/Ignitor.ts +59 -4
- package/src/bootstrap/hyper-server.ts +1 -1
- package/src/helpers/compose.ts +84 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/is.ts +88 -22
- package/src/index.ts +2 -2
- package/src/middleware/Pipeline.ts +10 -4
- package/src/rpc/RpcRouter.ts +2 -2
- package/src/scheduler/Scheduler.ts +10 -4
- package/src/services/app.ts +9 -0
- package/src/services/config.ts +24 -0
- package/src/services/encryption.ts +42 -0
- package/src/services/logger.ts +49 -0
- package/src/services/urlBuilder.ts +41 -0
- package/src/session/Session.ts +2 -2
- package/src/testing/ApiResponse.ts +52 -52
- package/src/testing/RequestBuilder.ts +55 -55
- package/src/testing/TestClient.ts +55 -13
- package/src/testing/testUtils.ts +90 -0
package/src/rpc/RpcRouter.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from '@c9up/comet'
|
|
21
21
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
22
22
|
import type { MiddlewareRegistry, RuntimeValidator } from '../middleware/Pipeline.js'
|
|
23
|
-
import {
|
|
23
|
+
import { composeMiddleware, runValidator } from '../middleware/Pipeline.js'
|
|
24
24
|
|
|
25
25
|
export type RpcHandler = (ctx: HttpContext, params: unknown) => Promise<unknown> | unknown
|
|
26
26
|
|
|
@@ -252,7 +252,7 @@ export class RpcRouter {
|
|
|
252
252
|
}
|
|
253
253
|
return mw
|
|
254
254
|
})
|
|
255
|
-
await
|
|
255
|
+
await composeMiddleware(fns)(ctx, async () => {})
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
async #processOne(ctx: HttpContext, request: unknown): Promise<unknown> {
|
|
@@ -252,9 +252,12 @@ export class Scheduler {
|
|
|
252
252
|
// `.then`-based detection + `Promise.resolve` wrap so non-native
|
|
253
253
|
// thenables (e.g. RxJS-style) are handled uniformly.
|
|
254
254
|
if (
|
|
255
|
+
// Object OR function: a thenable function is exotic but legal, and the
|
|
256
|
+
// guard this replaced accepted one. `in` narrows either.
|
|
257
|
+
(typeof result === 'object' || typeof result === 'function') &&
|
|
255
258
|
result !== null &&
|
|
256
|
-
|
|
257
|
-
typeof
|
|
259
|
+
'then' in result &&
|
|
260
|
+
typeof result.then === 'function'
|
|
258
261
|
) {
|
|
259
262
|
Promise.resolve(result).catch(() => {})
|
|
260
263
|
}
|
|
@@ -334,9 +337,12 @@ export class Scheduler {
|
|
|
334
337
|
// reporter never crashes the ticker.
|
|
335
338
|
const result = this.#errorReporter(err, { taskName }) as unknown
|
|
336
339
|
if (
|
|
340
|
+
// Object OR function: a thenable function is exotic but legal, and the
|
|
341
|
+
// guard this replaced accepted one. `in` narrows either.
|
|
342
|
+
(typeof result === 'object' || typeof result === 'function') &&
|
|
337
343
|
result !== null &&
|
|
338
|
-
|
|
339
|
-
typeof
|
|
344
|
+
'then' in result &&
|
|
345
|
+
typeof result.then === 'function'
|
|
340
346
|
) {
|
|
341
347
|
Promise.resolve(result).catch(() => {})
|
|
342
348
|
}
|
package/src/services/app.ts
CHANGED
|
@@ -26,6 +26,15 @@ export function clearApp(app: Application): void {
|
|
|
26
26
|
if (instance === app) instance = undefined
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @internal Get the app instance directly. Used by the accessors that stand in
|
|
31
|
+
* for something the app OWNS (`services/config` is `app.config`), so they read
|
|
32
|
+
* through here instead of keeping a second copy that a re-boot could desync.
|
|
33
|
+
*/
|
|
34
|
+
export function getApp(): Application | undefined {
|
|
35
|
+
return instance
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
const app: Application = createServiceProxy<Application>(
|
|
30
39
|
() => instance,
|
|
31
40
|
'Application accessed before initialization. ' +
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config store singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import config from '@c9up/ream/services/config'
|
|
6
|
+
* const port = config.get<number>('app.port', 3333)
|
|
7
|
+
*
|
|
8
|
+
* Unlike the other accessors this one keeps no instance of its own: the store
|
|
9
|
+
* IS `app.config`, so it reads through the app locator. One source of truth
|
|
10
|
+
* means the two can never point at different stores after a re-boot.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConfigStore } from '../ConfigLoader.js'
|
|
14
|
+
import { getApp } from './app.js'
|
|
15
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
16
|
+
|
|
17
|
+
const config: ConfigStore = createServiceProxy<ConfigStore>(
|
|
18
|
+
() => getApp()?.config,
|
|
19
|
+
'Config accessed before initialization. ' +
|
|
20
|
+
'The store is built during the boot phase — read it from inside a provider, ' +
|
|
21
|
+
'a preload, or a request, not at module top level.',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export default config
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie/value signing singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import encryption from '@c9up/ream/services/encryption'
|
|
6
|
+
* const signed = encryption.sign('payload')
|
|
7
|
+
*
|
|
8
|
+
* Registered only when `APP_KEY` is set — there is nothing to sign with
|
|
9
|
+
* otherwise — so the not-ready message names that rather than the boot phase.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CookieSigner } from '../security/CookieSigner.js'
|
|
13
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
14
|
+
|
|
15
|
+
let instance: CookieSigner | undefined
|
|
16
|
+
|
|
17
|
+
/** @internal Set the signer (called by Ignitor once APP_KEY is validated). */
|
|
18
|
+
export function setEncryption(signer: CookieSigner): void {
|
|
19
|
+
instance = signer
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal Unset the locator IF it still points at `signer` (called by
|
|
24
|
+
* Ignitor.stop()). Ownership-guarded — see services/app.ts.
|
|
25
|
+
*/
|
|
26
|
+
export function clearEncryption(signer: CookieSigner): void {
|
|
27
|
+
if (instance === signer) instance = undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @internal Get the signer directly. */
|
|
31
|
+
export function getEncryption(): CookieSigner | undefined {
|
|
32
|
+
return instance
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const encryption: CookieSigner = createServiceProxy<CookieSigner>(
|
|
36
|
+
() => instance,
|
|
37
|
+
'[E_MISSING_APP_KEY] Encryption accessed before it was registered. ' +
|
|
38
|
+
'It exists only when APP_KEY is set (>= 16 characters) — check the ' +
|
|
39
|
+
'environment before reaching for it, or you are reading it before boot.',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export default encryption
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application logger singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import logger from '@c9up/ream/services/logger'
|
|
6
|
+
* logger.info('user signed in', { userId })
|
|
7
|
+
*
|
|
8
|
+
* ream does not implement the logger — it declares the contract
|
|
9
|
+
* ({@link ContextLogger}) and reads whatever a provider bound as `'logger'`.
|
|
10
|
+
* `@c9up/spectrum` is the implementation in this universe, the way `hash` is
|
|
11
|
+
* sigil's and validation is rune's.
|
|
12
|
+
*
|
|
13
|
+
* `ctx.logger` stays the right thing to use inside a request: it is
|
|
14
|
+
* correlation-scoped. This is for everywhere else — a provider, a scheduled
|
|
15
|
+
* task, a console command — where there is no request to scope to.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ChildLoggerSource } from '../http/HttpContext.js'
|
|
19
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
20
|
+
|
|
21
|
+
let instance: ChildLoggerSource | undefined
|
|
22
|
+
|
|
23
|
+
/** @internal Set the logger (called by Ignitor once providers have booted). */
|
|
24
|
+
export function setLogger(logger: ChildLoggerSource): void {
|
|
25
|
+
instance = logger
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @internal Unset the locator IF it still points at `logger` (called by
|
|
30
|
+
* Ignitor.stop()). Ownership-guarded — see services/app.ts.
|
|
31
|
+
*/
|
|
32
|
+
export function clearLogger(logger: ChildLoggerSource): void {
|
|
33
|
+
if (instance === logger) instance = undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @internal Get the logger directly. */
|
|
37
|
+
export function getLogger(): ChildLoggerSource | undefined {
|
|
38
|
+
return instance
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const logger: ChildLoggerSource = createServiceProxy<ChildLoggerSource>(
|
|
42
|
+
() => instance,
|
|
43
|
+
'Logger accessed before initialization. ' +
|
|
44
|
+
'ream declares the contract but implements no logger — install one (`@c9up/spectrum`) ' +
|
|
45
|
+
"and register its provider, which binds it as 'logger'. Inside a request, prefer " +
|
|
46
|
+
'`ctx.logger`, which is scoped to the correlation id.',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
export default logger
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-route URL helpers, as free functions.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { urlFor, signedUrlFor } from '@c9up/ream/services/urlBuilder'
|
|
6
|
+
* const href = urlFor('users.show', { id: '42' })
|
|
7
|
+
* const link = signedUrlFor('email.verify', { id: '42' }, { expiresIn: '1h' })
|
|
8
|
+
*
|
|
9
|
+
* Both delegate to the router — the same code `router.urlFor` /
|
|
10
|
+
* `router.makeSignedUrl` run — so a route renamed in one place cannot mean two
|
|
11
|
+
* things. They exist as functions rather than as another proxy object because
|
|
12
|
+
* that is all there is to the URL builder: two calls, no state.
|
|
13
|
+
*
|
|
14
|
+
* `signedUrlFor` needs an APP_KEY-backed signer and says so if there is none.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { SignedUrlOptions } from '../router/Router.js'
|
|
18
|
+
import { getRouter } from './router.js'
|
|
19
|
+
|
|
20
|
+
const NOT_READY =
|
|
21
|
+
'URL builder used before initialization. ' +
|
|
22
|
+
'Routes are registered during the boot phase — build URLs from inside a ' +
|
|
23
|
+
'controller, a view, or a preload, not at module top level.'
|
|
24
|
+
|
|
25
|
+
/** Generate a URL from a named route, filling `:param` placeholders. */
|
|
26
|
+
export function urlFor(name: string, params?: Record<string, string>): string {
|
|
27
|
+
const router = getRouter()
|
|
28
|
+
if (!router) throw new Error(NOT_READY)
|
|
29
|
+
return router.urlFor(name, params)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generate a tamper-proof signed URL for a named route. */
|
|
33
|
+
export function signedUrlFor(
|
|
34
|
+
name: string,
|
|
35
|
+
params?: Record<string, string>,
|
|
36
|
+
options?: SignedUrlOptions,
|
|
37
|
+
): string {
|
|
38
|
+
const router = getRouter()
|
|
39
|
+
if (!router) throw new Error(NOT_READY)
|
|
40
|
+
return router.makeSignedUrl(name, params, options)
|
|
41
|
+
}
|
package/src/session/Session.ts
CHANGED
|
@@ -56,9 +56,9 @@ export interface SessionDriverWithTagging extends SessionDriver {
|
|
|
56
56
|
export function supportsTagging(driver: SessionDriver): driver is SessionDriverWithTagging {
|
|
57
57
|
return (
|
|
58
58
|
'tag' in driver &&
|
|
59
|
-
typeof
|
|
59
|
+
typeof driver.tag === 'function' &&
|
|
60
60
|
'tagged' in driver &&
|
|
61
|
-
typeof
|
|
61
|
+
typeof driver.tagged === 'function'
|
|
62
62
|
)
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rich HTTP test response —
|
|
2
|
+
* Rich HTTP test response — `helix`'s `ApiResponse` parity.
|
|
3
3
|
*
|
|
4
|
-
* Wraps the raw {@link TestResponse} and exposes the full
|
|
4
|
+
* Wraps the raw {@link TestResponse} and exposes the full helix response surface:
|
|
5
5
|
* accessor METHODS (`status()`, `headers()`, `body()`, `text()`, `header()`,
|
|
6
|
-
* `cookies()`, `redirects()`, `error()`, …) matching
|
|
6
|
+
* `cookies()`, `redirects()`, `error()`, …) matching helix exactly, the complete
|
|
7
7
|
* `assert*` family, response `dump*()` debuggers, and `macro()`/`getter()` for
|
|
8
8
|
* consumer extension.
|
|
9
9
|
*
|
|
10
|
-
* `json()` is kept as an additive convenience (not in
|
|
11
|
-
* parity —
|
|
10
|
+
* `json()` is kept as an additive convenience (not in helix, but harmless for
|
|
11
|
+
* parity — helix code never calls it) so `res.json()` call sites need no change.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { Dict } from '../types/helpers.js'
|
|
@@ -109,7 +109,7 @@ function parseCookies(setCookie: string | undefined): Record<string, ResponseCoo
|
|
|
109
109
|
return out
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** A file parsed from a `multipart/form-data` response —
|
|
112
|
+
/** A file parsed from a `multipart/form-data` response — helix `files()` entry. */
|
|
113
113
|
export interface ResponseFile {
|
|
114
114
|
/** The form field name. */
|
|
115
115
|
fieldName: string
|
|
@@ -123,7 +123,7 @@ export interface ResponseFile {
|
|
|
123
123
|
content: Buffer
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/** Consumer-registered response parsers (
|
|
126
|
+
/** Consumer-registered response parsers (helix `ApiRequest.addParser`), by content-type. */
|
|
127
127
|
const responseParsers = new Map<string, (body: string, headers: Dict) => unknown>()
|
|
128
128
|
|
|
129
129
|
/**
|
|
@@ -188,8 +188,8 @@ function splitBuffer(buf: Buffer, delim: Buffer): Buffer[] {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Validator for `assertAgainstApiSpec()` — mirrors
|
|
192
|
-
* is a SEPARATE opt-in (
|
|
191
|
+
* Validator for `assertAgainstApiSpec()` — mirrors helix's OpenAPI plugin, which
|
|
192
|
+
* is a SEPARATE opt-in (`a separate OpenAPI-assertions plugin`). Register one to enable spec
|
|
193
193
|
* assertions; without it, the assertion throws a clear "not configured" error
|
|
194
194
|
* rather than silently passing.
|
|
195
195
|
*/
|
|
@@ -206,7 +206,7 @@ function jsonOf(raw: TestResponse): unknown {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/** The
|
|
209
|
+
/** The helix error object returned by `response.error()`. */
|
|
210
210
|
export interface ResponseError {
|
|
211
211
|
/** The error status code (>= 400). */
|
|
212
212
|
status: number
|
|
@@ -216,14 +216,14 @@ export interface ResponseError {
|
|
|
216
216
|
|
|
217
217
|
/** Extra context the builder threads into the response. */
|
|
218
218
|
export interface ApiResponseMeta {
|
|
219
|
-
/** The (final) request method —
|
|
219
|
+
/** The (final) request method — helix `response.method()`. */
|
|
220
220
|
method?: string
|
|
221
|
-
/** URLs followed when redirects were configured —
|
|
221
|
+
/** URLs followed when redirects were configured — helix `response.redirects()`. */
|
|
222
222
|
redirects?: string[]
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* Every
|
|
226
|
+
* Every helix status-shortcut assertion, as `methodName → code`. Consumed to
|
|
227
227
|
* generate the `assert*` methods on both {@link ApiResponse} and the builder,
|
|
228
228
|
* so the two surfaces never drift.
|
|
229
229
|
*/
|
|
@@ -254,21 +254,21 @@ export const STATUS_ASSERTIONS = {
|
|
|
254
254
|
assertUnprocessableEntity: 422,
|
|
255
255
|
assertLocked: 423,
|
|
256
256
|
assertTooManyRequests: 429,
|
|
257
|
-
/** ream extra (not in
|
|
257
|
+
/** ream extra (not in helix's list, kept for convenience). */
|
|
258
258
|
assertInternalServerError: 500,
|
|
259
259
|
} as const
|
|
260
260
|
|
|
261
|
-
/** Consumer-registered macros/getters (
|
|
261
|
+
/** Consumer-registered macros/getters (helix `ApiResponse.macro`/`.getter`). */
|
|
262
262
|
const responseMacros = new Map<string, unknown>()
|
|
263
263
|
const responseGetters = new Map<string, (this: ApiResponse, res: ApiResponse) => unknown>()
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* The rich response. Constructed by the request builder from a raw
|
|
267
|
-
* {@link TestResponse}; exposes the
|
|
267
|
+
* {@link TestResponse}; exposes the helix accessor + assertion surface.
|
|
268
268
|
*
|
|
269
269
|
* PARITY NOTE: the status code / headers / raw text are METHODS — `status()`,
|
|
270
270
|
* `headers()`, `text()` — and `body()` returns the content-type-parsed body,
|
|
271
|
-
* exactly like
|
|
271
|
+
* exactly like helix. `json()` is kept as an additive convenience.
|
|
272
272
|
*/
|
|
273
273
|
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the merged interface (StatusAssertions) types the status-shortcut asserts attached to the prototype from STATUS_ASSERTIONS — the same generated-method pattern as EonSchema/Macroable (AdonisJS parity); every member is implemented, so the merge is safe.
|
|
274
274
|
export class ApiResponse {
|
|
@@ -283,35 +283,35 @@ export class ApiResponse {
|
|
|
283
283
|
applyResponseExtensions(this)
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
// ──
|
|
286
|
+
// ── helix accessors (method form) ─────────────────────────────────────────
|
|
287
287
|
|
|
288
|
-
/** HTTP status code —
|
|
288
|
+
/** HTTP status code — helix `status()`. */
|
|
289
289
|
status(): number {
|
|
290
290
|
return this.#raw.status
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
/** Status class digit (2 for 2xx, 4 for 4xx, …) —
|
|
293
|
+
/** Status class digit (2 for 2xx, 4 for 4xx, …) — helix `statusType()`. */
|
|
294
294
|
statusType(): number {
|
|
295
295
|
return Math.floor(this.#raw.status / 100)
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
/** All response headers (lower-cased keys) —
|
|
298
|
+
/** All response headers (lower-cased keys) — helix `headers()`. */
|
|
299
299
|
headers(): Dict {
|
|
300
300
|
return this.#raw.headers
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
/** A single response header value —
|
|
303
|
+
/** A single response header value — helix `header(name)`. */
|
|
304
304
|
header(name: string): string | undefined {
|
|
305
305
|
return this.#raw.headers[name.toLowerCase()]
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
/** Raw response text —
|
|
308
|
+
/** Raw response text — helix `text()`. */
|
|
309
309
|
text(): string {
|
|
310
310
|
return this.#raw.body
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
|
-
* The parsed response body —
|
|
314
|
+
* The parsed response body — helix `body()`. Parses by `Content-Type`:
|
|
315
315
|
* `application/json` → object, `application/x-www-form-urlencoded` → object,
|
|
316
316
|
* `multipart/form-data` → the non-file fields (files come from {@link files});
|
|
317
317
|
* otherwise the raw text. A registered parser (`ApiResponse.addParser`) for the
|
|
@@ -336,45 +336,45 @@ export class ApiResponse {
|
|
|
336
336
|
return text
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
/** Convenience JSON parse (additive; not in
|
|
339
|
+
/** Convenience JSON parse (additive; not in helix). */
|
|
340
340
|
json<T = unknown>(): T {
|
|
341
341
|
return JSON.parse(this.#raw.body) as T
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
/** The request method that produced this response —
|
|
344
|
+
/** The request method that produced this response — helix `method()`. */
|
|
345
345
|
method(): string {
|
|
346
346
|
return this.#method
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
/** Response `Content-Type` without parameters —
|
|
349
|
+
/** Response `Content-Type` without parameters — helix `type()`. */
|
|
350
350
|
type(): string | undefined {
|
|
351
351
|
const ct = this.#raw.headers['content-type']
|
|
352
352
|
return ct ? (ct.split(';')[0]?.trim() ?? ct) : undefined
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
/** Charset from `Content-Type`, if any —
|
|
355
|
+
/** Charset from `Content-Type`, if any — helix `charset()`. */
|
|
356
356
|
charset(): string | undefined {
|
|
357
357
|
const ct = this.#raw.headers['content-type'] ?? ''
|
|
358
358
|
const m = ct.match(/charset=([^;]+)/i)
|
|
359
359
|
return m ? m[1].trim() : undefined
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
/** All response cookies, parsed from `Set-Cookie` —
|
|
362
|
+
/** All response cookies, parsed from `Set-Cookie` — helix `cookies()`. */
|
|
363
363
|
cookies(): Record<string, ResponseCookie> {
|
|
364
364
|
return parseCookies(this.#raw.headers['set-cookie'])
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
/** A single parsed response cookie —
|
|
367
|
+
/** A single parsed response cookie — helix `cookie(name)`. */
|
|
368
368
|
cookie(name: string): ResponseCookie | undefined {
|
|
369
369
|
return this.cookies()[name]
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
/** URLs followed for a redirected request —
|
|
372
|
+
/** URLs followed for a redirected request — helix `redirects()`. */
|
|
373
373
|
redirects(): string[] {
|
|
374
374
|
return [...this.#redirects]
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
/** Parsed `Link` header (`rel → url`) —
|
|
377
|
+
/** Parsed `Link` header (`rel → url`) — helix `links()`. */
|
|
378
378
|
links(): Record<string, string> {
|
|
379
379
|
const link = this.#raw.headers.link
|
|
380
380
|
if (!link) return {}
|
|
@@ -387,7 +387,7 @@ export class ApiResponse {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
/**
|
|
390
|
-
* Files parsed from a `multipart/form-data` response —
|
|
390
|
+
* Files parsed from a `multipart/form-data` response — helix `files()`. Each
|
|
391
391
|
* entry carries the field name, filename, content-type and the raw bytes.
|
|
392
392
|
* Empty for non-multipart responses.
|
|
393
393
|
*/
|
|
@@ -398,23 +398,23 @@ export class ApiResponse {
|
|
|
398
398
|
return parseMultipartResponse(this.#raw.body, ctRaw).files
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
/** Whether the response carries a body —
|
|
401
|
+
/** Whether the response carries a body — helix `hasBody()`. */
|
|
402
402
|
hasBody(): boolean {
|
|
403
403
|
return this.#raw.body.length > 0
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
/** Whether the status is an error (>= 400) —
|
|
406
|
+
/** Whether the status is an error (>= 400) — helix `hasError()`. */
|
|
407
407
|
hasError(): boolean {
|
|
408
408
|
return this.#raw.status >= 400
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
/** Whether the status is a server error (>= 500) —
|
|
411
|
+
/** Whether the status is a server error (>= 500) — helix `hasFatalError()`. */
|
|
412
412
|
hasFatalError(): boolean {
|
|
413
413
|
return this.#raw.status >= 500
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
|
-
* The error object when the response is an error, else `undefined` —
|
|
417
|
+
* The error object when the response is an error, else `undefined` — helix
|
|
418
418
|
* `error()`. Exposes `.status` and `.text` (superagent-style).
|
|
419
419
|
*/
|
|
420
420
|
error(): ResponseError | undefined {
|
|
@@ -422,9 +422,9 @@ export class ApiResponse {
|
|
|
422
422
|
return { status: this.#raw.status, text: this.#raw.body }
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
// ── Debugging (
|
|
425
|
+
// ── Debugging (helix response `dump*`) ────────────────────────────────────
|
|
426
426
|
|
|
427
|
-
/** Print status + headers + body to stderr —
|
|
427
|
+
/** Print status + headers + body to stderr — helix `response.dump()`. */
|
|
428
428
|
dump(): this {
|
|
429
429
|
process.stderr.write(
|
|
430
430
|
`[helix:response.dump] ${this.#method} → ${this.#raw.status}\n` +
|
|
@@ -433,26 +433,26 @@ export class ApiResponse {
|
|
|
433
433
|
)
|
|
434
434
|
return this
|
|
435
435
|
}
|
|
436
|
-
/** Print only the response body —
|
|
436
|
+
/** Print only the response body — helix `response.dumpBody()`. */
|
|
437
437
|
dumpBody(): this {
|
|
438
438
|
process.stderr.write(`[helix:response.dumpBody] ${capBody(this.#raw.body, 2048)}\n`)
|
|
439
439
|
return this
|
|
440
440
|
}
|
|
441
|
-
/** Print only the response headers —
|
|
441
|
+
/** Print only the response headers — helix `response.dumpHeaders()`. */
|
|
442
442
|
dumpHeaders(): this {
|
|
443
443
|
process.stderr.write(
|
|
444
444
|
`[helix:response.dumpHeaders] ${JSON.stringify(this.#raw.headers, null, 2)}\n`,
|
|
445
445
|
)
|
|
446
446
|
return this
|
|
447
447
|
}
|
|
448
|
-
/** Print only the response cookies —
|
|
448
|
+
/** Print only the response cookies — helix `response.dumpCookies()`. */
|
|
449
449
|
dumpCookies(): this {
|
|
450
450
|
process.stderr.write(
|
|
451
451
|
`[helix:response.dumpCookies] ${JSON.stringify(this.cookies(), null, 2)}\n`,
|
|
452
452
|
)
|
|
453
453
|
return this
|
|
454
454
|
}
|
|
455
|
-
/** Print the error object, if any —
|
|
455
|
+
/** Print the error object, if any — helix `response.dumpError()`. */
|
|
456
456
|
dumpError(): this {
|
|
457
457
|
process.stderr.write(`[helix:response.dumpError] ${JSON.stringify(this.error(), null, 2)}\n`)
|
|
458
458
|
return this
|
|
@@ -522,7 +522,7 @@ export class ApiResponse {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/**
|
|
525
|
-
* The body used by `assertBody*`, parsed per `Content-Type` (
|
|
525
|
+
* The body used by `assertBody*`, parsed per `Content-Type` (helix parity):
|
|
526
526
|
* urlencoded/multipart → the structured `body()`; JSON or an unknown/absent
|
|
527
527
|
* content-type → a tolerant JSON parse (so bodyless test fakes keep working).
|
|
528
528
|
*/
|
|
@@ -574,7 +574,7 @@ export class ApiResponse {
|
|
|
574
574
|
}
|
|
575
575
|
|
|
576
576
|
/**
|
|
577
|
-
* Assert the request was redirected to `path` —
|
|
577
|
+
* Assert the request was redirected to `path` — helix `assertRedirectsTo`.
|
|
578
578
|
* Checks the followed redirect chain (`redirects()`); also accepts a direct
|
|
579
579
|
* 3xx `Location` match when no redirects were followed.
|
|
580
580
|
*/
|
|
@@ -599,9 +599,9 @@ export class ApiResponse {
|
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
/**
|
|
602
|
-
* Assert the response conforms to the registered OpenAPI spec —
|
|
603
|
-
* `assertAgainstApiSpec()`.
|
|
604
|
-
*
|
|
602
|
+
* Assert the response conforms to the registered OpenAPI spec — helix
|
|
603
|
+
* `assertAgainstApiSpec()`. helix ships this via the SEPARATE opt-in
|
|
604
|
+
* `a separate OpenAPI-assertions plugin` plugin; register a validator with
|
|
605
605
|
* {@link ApiResponse.registerApiSpecValidator} to enable it. Without one, this
|
|
606
606
|
* throws (never silently passes).
|
|
607
607
|
*/
|
|
@@ -609,7 +609,7 @@ export class ApiResponse {
|
|
|
609
609
|
if (!apiSpecValidator) {
|
|
610
610
|
throw new ExpectationError(
|
|
611
611
|
'assertAgainstApiSpec() requires an OpenAPI validator. Register one via ' +
|
|
612
|
-
'ApiResponse.registerApiSpecValidator(fn) (the opt-in equivalent of
|
|
612
|
+
'ApiResponse.registerApiSpecValidator(fn) (the opt-in equivalent of a separate OpenAPI-assertions plugin).',
|
|
613
613
|
)
|
|
614
614
|
}
|
|
615
615
|
apiSpecValidator(this)
|
|
@@ -617,9 +617,9 @@ export class ApiResponse {
|
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
/**
|
|
620
|
-
* Register a shared property (
|
|
620
|
+
* Register a shared property (helix `ApiResponse.macro`) or lazy getter
|
|
621
621
|
* (`ApiResponse.getter`). The callback is invoked with `this` bound to the
|
|
622
|
-
* response AND the response as its first argument, so both
|
|
622
|
+
* response AND the response as its first argument, so both helix's
|
|
623
623
|
* `function () { return this.header('x') }` and `(res) => res.header('x')` work.
|
|
624
624
|
*/
|
|
625
625
|
static macro(name: string, value: unknown): void {
|
|
@@ -629,7 +629,7 @@ export class ApiResponse {
|
|
|
629
629
|
responseGetters.set(name, fn)
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
/** Register a response body parser for a content-type —
|
|
632
|
+
/** Register a response body parser for a content-type — helix `ApiRequest.addParser`. */
|
|
633
633
|
static addParser(contentType: string, fn: (body: string, headers: Dict) => unknown): void {
|
|
634
634
|
responseParsers.set(contentType, fn)
|
|
635
635
|
}
|