@c9up/ream 0.2.14 → 0.2.16
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 +1 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +6 -1
- package/dist/Application.js.map +1 -1
- package/dist/Ignitor.d.ts +9 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +29 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +12 -1
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +7 -3
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/config.d.ts +15 -0
- package/dist/bodyparser/config.d.ts.map +1 -0
- package/dist/bodyparser/config.js +16 -0
- package/dist/bodyparser/config.js.map +1 -0
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +3 -2
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/commands/MigrateBase.d.ts.map +1 -1
- package/dist/commands/MigrateBase.js +4 -1
- package/dist/commands/MigrateBase.js.map +1 -1
- package/dist/config/moduleConfig.d.ts +15 -0
- package/dist/config/moduleConfig.d.ts.map +1 -0
- package/dist/config/moduleConfig.js +24 -0
- package/dist/config/moduleConfig.js.map +1 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +7 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/cliui.d.ts +2 -0
- package/dist/console/cliui.d.ts.map +1 -1
- package/dist/console/cliui.js +2 -1
- package/dist/console/cliui.js.map +1 -1
- package/dist/console/types.d.ts +14 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/console/types.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +6 -1
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +11 -1
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -1
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +4 -2
- package/dist/env/Env.js.map +1 -1
- package/dist/env/interpolate.d.ts.map +1 -1
- package/dist/env/interpolate.js +5 -3
- package/dist/env/interpolate.js.map +1 -1
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
- package/dist/errors/FuzzyMatcher.js +30 -13
- package/dist/errors/FuzzyMatcher.js.map +1 -1
- package/dist/errors/PipelineStageError.d.ts.map +1 -1
- package/dist/errors/PipelineStageError.js +4 -2
- package/dist/errors/PipelineStageError.js.map +1 -1
- package/dist/events/Emitter.d.ts +21 -4
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +6 -2
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/testing/FakeBus.d.ts +1 -1
- package/dist/events/testing/FakeBus.d.ts.map +1 -1
- package/dist/events/testing/FakeBus.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +2 -2
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +4 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/bytes.d.ts.map +1 -1
- package/dist/helpers/bytes.js +13 -2
- package/dist/helpers/bytes.js.map +1 -1
- package/dist/helpers/string.js +3 -3
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/Exception.d.ts +8 -4
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +14 -8
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +10 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/mime.d.ts.map +1 -1
- package/dist/http/mime.js +2 -1
- package/dist/http/mime.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/native/generated.d.ts +12 -8
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.js +6 -4
- package/dist/openapi/OpenApiGenerator.js.map +1 -1
- package/dist/router/Router.d.ts +10 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +22 -10
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +16 -3
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +34 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Schedule.d.ts +6 -1
- package/dist/scheduler/Schedule.d.ts.map +1 -1
- package/dist/scheduler/Schedule.js +5 -0
- package/dist/scheduler/Schedule.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts +15 -11
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +21 -12
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +12 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.js +3 -2
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts +2 -4
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +13 -1
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +25 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +18 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +23 -3
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +10 -2
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +10 -11
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.js +6 -6
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +6 -2
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +4 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/dist/utils/objectPath.d.ts.map +1 -1
- package/dist/utils/objectPath.js +14 -6
- package/dist/utils/objectPath.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +13 -4
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-napi-types.mjs +28 -12
- package/src/Application.ts +6 -1
- package/src/Ignitor.ts +42 -3
- package/src/Provider.ts +21 -2
- package/src/bodyparser/BodyParserMiddleware.ts +12 -1
- package/src/bodyparser/MultipartFile.ts +7 -3
- package/src/bodyparser/config.ts +18 -0
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/config/moduleConfig.ts +24 -0
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- package/src/console/cliui.ts +2 -1
- package/src/console/types.ts +15 -0
- package/src/container/Container.ts +15 -6
- package/src/container/types.ts +11 -1
- package/src/decorators/Service.ts +1 -1
- package/src/env/Env.ts +4 -2
- package/src/env/interpolate.ts +5 -3
- package/src/errors/FuzzyMatcher.ts +34 -13
- package/src/errors/PipelineStageError.ts +4 -2
- package/src/events/Emitter.ts +34 -8
- package/src/events/testing/FakeBus.ts +12 -5
- package/src/graphql/GraphQLEngine.ts +11 -7
- package/src/helpers/bytes.ts +13 -2
- package/src/helpers/string.ts +3 -3
- package/src/http/Exception.ts +14 -8
- package/src/http/Request.ts +8 -5
- package/src/http/mime.ts +2 -1
- package/src/index.ts +2 -2
- package/src/native/generated.ts +12 -8
- package/src/openapi/OpenApiGenerator.ts +6 -4
- package/src/router/Router.ts +31 -11
- package/src/rpc/RpcRouter.ts +38 -8
- package/src/scheduler/Schedule.ts +8 -1
- package/src/scheduler/ScheduleProvider.ts +22 -12
- package/src/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- package/src/session/SessionMiddleware.ts +15 -2
- package/src/session/config.ts +29 -0
- package/src/session/drivers/DatabaseDriver.ts +36 -8
- package/src/session/drivers/RedisDriver.ts +13 -5
- package/src/testing/ApiResponse.ts +9 -10
- package/src/testing/TestClient.ts +6 -6
- package/src/utils/Macroable.ts +6 -2
- package/src/utils/objectPath.ts +12 -6
package/src/env/Env.ts
CHANGED
|
@@ -108,9 +108,11 @@ export class Env<Values extends Record<string, unknown>> {
|
|
|
108
108
|
validate(values: Record<string, string | undefined>): EnvRecord<S> {
|
|
109
109
|
const failures: string[] = []
|
|
110
110
|
const collected: Record<string, unknown> = {}
|
|
111
|
-
|
|
111
|
+
// `entries` rather than `keys` plus a lookup: the validator comes back
|
|
112
|
+
// already known to be there instead of read again as maybe-missing.
|
|
113
|
+
for (const [key, node] of Object.entries(schema)) {
|
|
112
114
|
try {
|
|
113
|
-
collected[key] =
|
|
115
|
+
collected[key] = node.validate(key, values[key])
|
|
114
116
|
} catch (err) {
|
|
115
117
|
failures.push(err instanceof Error ? err.message : String(err))
|
|
116
118
|
}
|
package/src/env/interpolate.ts
CHANGED
|
@@ -46,9 +46,11 @@ export function interpolate(value: string, lookup: (name: string) => string | un
|
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
const prefixed = /^([A-Za-z_][A-Za-z0-9_]*):([\s\S]*)$/.exec(substituted)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Both groups are required by the pattern, so a match carries both.
|
|
50
|
+
const [, identifier, rest] = prefixed ?? []
|
|
51
|
+
if (identifier !== undefined && rest !== undefined) {
|
|
52
|
+
const resolver = identifiers.get(identifier)
|
|
53
|
+
if (resolver) return resolver(rest)
|
|
52
54
|
}
|
|
53
55
|
return substituted
|
|
54
56
|
}
|
|
@@ -14,23 +14,43 @@ export function levenshtein(a: string, b: string): number {
|
|
|
14
14
|
if (m === 0) return n
|
|
15
15
|
if (n === 0) return m
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// Two rows rather than the whole matrix: the recurrence only ever reads the
|
|
18
|
+
// row above and the cell to the left, so the rest was allocated and never
|
|
19
|
+
// looked at again.
|
|
20
|
+
let previous: number[] = Array.from({ length: n + 1 }, (_, j) => j)
|
|
21
21
|
|
|
22
22
|
for (let i = 1; i <= m; i++) {
|
|
23
|
+
const current: number[] = [i]
|
|
23
24
|
for (let j = 1; j <= n; j++) {
|
|
24
25
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
current.push(
|
|
27
|
+
Math.min(
|
|
28
|
+
cell(previous, j) + 1, // deletion
|
|
29
|
+
cell(current, j - 1) + 1, // insertion
|
|
30
|
+
cell(previous, j - 1) + cost, // substitution
|
|
31
|
+
),
|
|
29
32
|
)
|
|
30
33
|
}
|
|
34
|
+
previous = current
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
return
|
|
37
|
+
return cell(previous, n)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One cell of a row that has already been filled.
|
|
42
|
+
*
|
|
43
|
+
* Each row is built left to right and every column is written before it is
|
|
44
|
+
* read, so a miss cannot happen — and this is where that invariant is stated
|
|
45
|
+
* rather than asserted past it. Reading the row directly would have every
|
|
46
|
+
* access typed "a number, or nothing", which is not what the algorithm knows.
|
|
47
|
+
*/
|
|
48
|
+
function cell(row: number[], index: number): number {
|
|
49
|
+
const value = row[index]
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
throw new RangeError(`levenshtein: column ${index} was read before it was written`)
|
|
52
|
+
}
|
|
53
|
+
return value
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
/**
|
|
@@ -81,12 +101,13 @@ export function findClosestMatches(
|
|
|
81
101
|
* @returns Formatted suggestion string, or empty string if no matches
|
|
82
102
|
*/
|
|
83
103
|
export function didYouMean(input: string, candidates: string[]): string {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
104
|
+
const [best, ...rest] = findClosestMatches(input, candidates)
|
|
105
|
+
if (best === undefined) return ''
|
|
86
106
|
|
|
87
|
-
if (
|
|
88
|
-
return `Did you mean '${
|
|
107
|
+
if (rest.length === 0) {
|
|
108
|
+
return `Did you mean '${best.candidate}'?`
|
|
89
109
|
}
|
|
110
|
+
const matches = [best, ...rest]
|
|
90
111
|
|
|
91
112
|
const suggestions = matches.map((m) => `'${m.candidate}'`).join(', ')
|
|
92
113
|
return `Did you mean one of: ${suggestions}?`
|
|
@@ -42,9 +42,11 @@ export function createPipelineError(
|
|
|
42
42
|
): ReamError {
|
|
43
43
|
const total = PIPELINE_STAGES.length
|
|
44
44
|
const clampedIndex = Math.max(0, Math.min(stageIndex, total - 1))
|
|
45
|
-
|
|
45
|
+
// The index is clamped into the list just above, so the lookup lands — the
|
|
46
|
+
// fallback is what says so without asserting past the read.
|
|
47
|
+
const stageName = PIPELINE_STAGES[clampedIndex] ?? '(unknown)'
|
|
46
48
|
const position = `${clampedIndex + 1}/${total}`
|
|
47
|
-
const nextStage =
|
|
49
|
+
const nextStage = PIPELINE_STAGES[clampedIndex + 1] ?? '(end)'
|
|
48
50
|
|
|
49
51
|
const errorCode = originalError instanceof ReamError ? originalError.code : 'E_PIPELINE_ERROR'
|
|
50
52
|
|
package/src/events/Emitter.ts
CHANGED
|
@@ -23,14 +23,25 @@ const correlationStorage = new AsyncLocalStorage<string | undefined>()
|
|
|
23
23
|
* Listener class interface — must have a handle() method.
|
|
24
24
|
*/
|
|
25
25
|
export interface ListenerClass<T = unknown> {
|
|
26
|
-
handle(event: T):
|
|
26
|
+
handle(event: T): unknown
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
type EventConstructor<T = unknown> = new (...args: never[]) => T
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A listener. Whatever it returns is awaited and dropped.
|
|
33
|
+
*
|
|
34
|
+
* `unknown`, not `Promise<void> | void`: a bare `void` return type accepts a
|
|
35
|
+
* function returning anything, but a UNION containing it does not — so
|
|
36
|
+
* `emitter.on('x', (d) => received.push(d))` was a type error for returning
|
|
37
|
+
* what `push` returns, which nothing reads.
|
|
38
|
+
*/
|
|
39
|
+
type ListenerFn<T = unknown> = (event: T) => unknown
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
// `abstract new`: a listener bound against an abstract base is as valid a
|
|
42
|
+
// registration as one bound against a concrete class, and the container builds
|
|
43
|
+
// either.
|
|
44
|
+
type ListenerConstructor<T = unknown> = abstract new (...args: never[]) => ListenerClass<T>
|
|
34
45
|
|
|
35
46
|
type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
|
|
36
47
|
|
|
@@ -41,7 +52,16 @@ type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
|
|
|
41
52
|
* satisfies it.
|
|
42
53
|
*/
|
|
43
54
|
export interface EmitterResolver {
|
|
44
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Build a listener.
|
|
57
|
+
*
|
|
58
|
+
* Not generic: a generic method must work for every `T`, so nothing but a
|
|
59
|
+
* real container could implement it — the fake in the tests could not, and
|
|
60
|
+
* this file's own call site reached for `as ListenerConstructor<T>` to get
|
|
61
|
+
* past it. What comes back is checked below, which is what a container's
|
|
62
|
+
* answer deserves anyway.
|
|
63
|
+
*/
|
|
64
|
+
make(target: abstract new (...args: never[]) => object): Promise<object>
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
/**
|
|
@@ -327,9 +347,15 @@ export class Emitter {
|
|
|
327
347
|
if (isListenerClass(listener)) {
|
|
328
348
|
// Listener class — resolve via container for @inject() support
|
|
329
349
|
const instance = this.#resolver
|
|
330
|
-
? await this.#resolver.make(listener
|
|
331
|
-
:
|
|
332
|
-
|
|
350
|
+
? await this.#resolver.make(listener)
|
|
351
|
+
: Reflect.construct(listener, [])
|
|
352
|
+
const handle = Reflect.get(instance, 'handle')
|
|
353
|
+
if (typeof handle !== 'function') {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Listener ${String(Reflect.get(listener, 'name'))} resolved to something with no handle()`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
await handle.call(instance, event)
|
|
333
359
|
} else {
|
|
334
360
|
await (listener as ListenerFn<T>)(event)
|
|
335
361
|
}
|
|
@@ -651,7 +677,7 @@ function classToEventName(cls: EventConstructor): string {
|
|
|
651
677
|
}
|
|
652
678
|
|
|
653
679
|
/** Check if a listener entry is a class (has prototype.handle) vs a function. */
|
|
654
|
-
function isListenerClass(listener: Listener):
|
|
680
|
+
function isListenerClass(listener: Listener): listener is ListenerConstructor {
|
|
655
681
|
return typeof listener === 'function' && typeof listener.prototype?.handle === 'function'
|
|
656
682
|
}
|
|
657
683
|
|
|
@@ -41,10 +41,17 @@ export type FakeBusPredicateArg = FakeBusPredicate | ((event: CapturedEmit) => b
|
|
|
41
41
|
|
|
42
42
|
interface Subscription {
|
|
43
43
|
pattern: string
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Subscribers may be sync or async; whatever they return is awaited and
|
|
46
|
+
* dropped, and an async rejection is logged via `console.error` rather than
|
|
47
|
+
* escaping to `unhandledRejection`.
|
|
48
|
+
*
|
|
49
|
+
* `unknown`, not `void | PromiseLike<void>`: a bare `void` return type
|
|
50
|
+
* accepts a function returning anything, but a UNION containing it does not
|
|
51
|
+
* — so widening the native bus's `=> void` to that union made it NARROWER in
|
|
52
|
+
* practice, and `(json) => received.push(json)` stopped compiling.
|
|
53
|
+
*/
|
|
54
|
+
callback: (eventJson: string) => unknown
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
/** Request handler may return `void` (sync, fire-and-forget after `reply`)
|
|
@@ -106,7 +113,7 @@ export class FakeBus {
|
|
|
106
113
|
return Promise.resolve('ok')
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
subscribe(pattern: string, callback: (eventJson: string) =>
|
|
116
|
+
subscribe(pattern: string, callback: (eventJson: string) => unknown): number {
|
|
110
117
|
const id = this.#nextSubId++
|
|
111
118
|
this.#subscribers.set(id, { pattern, callback })
|
|
112
119
|
return id
|
|
@@ -182,7 +182,7 @@ export interface ResolverOptions {
|
|
|
182
182
|
interface ResolverEntry {
|
|
183
183
|
typeName: string
|
|
184
184
|
fieldName: string
|
|
185
|
-
handlerClass: new (...args:
|
|
185
|
+
handlerClass: abstract new (...args: never[]) => object
|
|
186
186
|
methodName: string
|
|
187
187
|
options: ResolverOptions
|
|
188
188
|
}
|
|
@@ -204,7 +204,7 @@ export interface SelectionField {
|
|
|
204
204
|
export class GraphQLEngine {
|
|
205
205
|
#schemaSource: string
|
|
206
206
|
#resolvers: Map<string, ResolverEntry> = new Map()
|
|
207
|
-
#container?: { make<T>(target: new (...args:
|
|
207
|
+
#container?: { make<T>(target: abstract new (...args: never[]) => T): Promise<T> }
|
|
208
208
|
readonly path: string
|
|
209
209
|
#playground: boolean
|
|
210
210
|
#maxQueryBytes: number
|
|
@@ -239,7 +239,9 @@ export class GraphQLEngine {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/** Set IoC container for resolver instantiation. */
|
|
242
|
-
useContainer(container: {
|
|
242
|
+
useContainer(container: {
|
|
243
|
+
make<T>(target: abstract new (...args: never[]) => T): Promise<T>
|
|
244
|
+
}): void {
|
|
243
245
|
this.#container = container
|
|
244
246
|
}
|
|
245
247
|
|
|
@@ -247,7 +249,7 @@ export class GraphQLEngine {
|
|
|
247
249
|
resolver(
|
|
248
250
|
typeName: string,
|
|
249
251
|
fieldName: string,
|
|
250
|
-
handlerClass: new (...args:
|
|
252
|
+
handlerClass: abstract new (...args: never[]) => object,
|
|
251
253
|
methodName: string,
|
|
252
254
|
options?: ResolverOptions,
|
|
253
255
|
): void {
|
|
@@ -369,11 +371,13 @@ export class GraphQLEngine {
|
|
|
369
371
|
|
|
370
372
|
try {
|
|
371
373
|
// Resolve handler via IoC or direct instantiation.
|
|
372
|
-
|
|
374
|
+
// `Reflect.construct`, not `new`: the resolver may be an abstract class,
|
|
375
|
+
// which the runtime builds happily and `new` refuses at compile time.
|
|
376
|
+
const instance: object = this.#container
|
|
373
377
|
? await this.#container.make(entry.handlerClass)
|
|
374
|
-
:
|
|
378
|
+
: Reflect.construct(entry.handlerClass, [])
|
|
375
379
|
|
|
376
|
-
const handler = instance
|
|
380
|
+
const handler = Reflect.get(instance, entry.methodName)
|
|
377
381
|
if (typeof handler !== 'function') {
|
|
378
382
|
return {
|
|
379
383
|
ok: false,
|
package/src/helpers/bytes.ts
CHANGED
|
@@ -18,6 +18,11 @@ const UNITS = {
|
|
|
18
18
|
|
|
19
19
|
type Unit = keyof typeof UNITS
|
|
20
20
|
|
|
21
|
+
/** A suffix the table knows. Checked, so nothing has to be asserted. */
|
|
22
|
+
function isUnit(value: string): value is Unit {
|
|
23
|
+
return value in UNITS
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
const PARSE = /^((?:-|\+)?(?:\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i
|
|
22
27
|
/** Trailing `.00`, or the zeros trailing a real decimal. */
|
|
23
28
|
const TRAILING_ZEROS = /(?:\.0*|(\.[^0]+)0+)$/
|
|
@@ -33,10 +38,16 @@ export function parse(value: string | number): number | null {
|
|
|
33
38
|
if (typeof value !== 'string') return null
|
|
34
39
|
|
|
35
40
|
const matched = PARSE.exec(value)
|
|
36
|
-
|
|
41
|
+
// Both groups are required by the pattern, so a match carries both — but
|
|
42
|
+
// read as `matched[2]` the second was typed "a string, or nothing", and the
|
|
43
|
+
// `as Unit` that followed asserted past exactly that. Named, with the
|
|
44
|
+
// suffix checked against the table it indexes, nothing is asserted.
|
|
45
|
+
const [, digits, suffix] = matched ?? []
|
|
46
|
+
const amount = digits !== undefined ? Number.parseFloat(digits) : Number.parseInt(value, 10)
|
|
37
47
|
if (Number.isNaN(amount)) return null
|
|
38
48
|
|
|
39
|
-
const unit =
|
|
49
|
+
const unit = suffix === undefined ? 'b' : suffix.toLowerCase()
|
|
50
|
+
if (!isUnit(unit)) return null
|
|
40
51
|
return Math.floor(UNITS[unit] * amount)
|
|
41
52
|
}
|
|
42
53
|
|
package/src/helpers/string.ts
CHANGED
|
@@ -214,9 +214,9 @@ export function pluralize(word: string, count?: number): string {
|
|
|
214
214
|
* @example humanize('user_name') // 'User name'
|
|
215
215
|
*/
|
|
216
216
|
export function humanize(s: string): string {
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
return [
|
|
217
|
+
const [first, ...rest] = splitWords(s)
|
|
218
|
+
if (first === undefined) return ''
|
|
219
|
+
return [first.charAt(0).toUpperCase() + first.slice(1), ...rest].join(' ')
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// ---------------------------------------------------------------------------
|
package/src/http/Exception.ts
CHANGED
|
@@ -315,8 +315,10 @@ export class ExceptionHandler extends Macroable {
|
|
|
315
315
|
#expandStatusPages(): Record<number, StatusPageRenderer> {
|
|
316
316
|
if (!this.#expandedStatusPages) {
|
|
317
317
|
const expanded: Record<number, StatusPageRenderer> = {}
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
// `entries` rather than `keys` plus a lookup: the value comes back
|
|
319
|
+
// already known to be there, instead of read again as maybe-missing.
|
|
320
|
+
for (const [range, renderer] of Object.entries(this.statusPages)) {
|
|
321
|
+
Object.assign(expanded, parseStatusRange(range, renderer))
|
|
320
322
|
}
|
|
321
323
|
this.#expandedStatusPages = expanded
|
|
322
324
|
}
|
|
@@ -440,13 +442,17 @@ export class ExceptionHandler extends Macroable {
|
|
|
440
442
|
/**
|
|
441
443
|
* Pick a renderer by content negotiation.
|
|
442
444
|
*
|
|
443
|
-
*
|
|
444
|
-
* Accept
|
|
445
|
-
*
|
|
446
|
-
*
|
|
445
|
+
* `html` is listed first, as AdonisJS does, so a request that states no
|
|
446
|
+
* preference — no `Accept` at all, or `*` /`*` — gets the error page rather
|
|
447
|
+
* than a JSON body. A browser always sends `text/html` and an API client
|
|
448
|
+
* always asks for JSON, so the order only decides for the clients in
|
|
449
|
+
* between: curl, a health probe, a proxy that stripped the header. For a
|
|
450
|
+
* framework that ships a view layer, a page is the better answer there.
|
|
451
|
+
*
|
|
452
|
+
* The fallback stays JSON, for a client that accepts none of the three.
|
|
447
453
|
*/
|
|
448
454
|
async renderError(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
449
|
-
switch (ctx.request.accepts(['
|
|
455
|
+
switch (ctx.request.accepts(['html', 'json', 'application/vnd.api+json'])) {
|
|
450
456
|
case 'application/vnd.api+json':
|
|
451
457
|
return this.renderErrorAsJSONAPI(error, ctx)
|
|
452
458
|
case 'html':
|
|
@@ -458,7 +464,7 @@ export class ExceptionHandler extends Macroable {
|
|
|
458
464
|
|
|
459
465
|
/** Pick a validation renderer by content negotiation. */
|
|
460
466
|
async renderValidationError(error: HttpError, ctx: HttpContext): Promise<void> {
|
|
461
|
-
switch (ctx.request.accepts(['
|
|
467
|
+
switch (ctx.request.accepts(['html', 'json', 'application/vnd.api+json'])) {
|
|
462
468
|
case 'application/vnd.api+json':
|
|
463
469
|
return this.renderValidationErrorAsJSONAPI(error, ctx)
|
|
464
470
|
case 'html':
|
package/src/http/Request.ts
CHANGED
|
@@ -272,7 +272,10 @@ export class Request extends Macroable {
|
|
|
272
272
|
host(): string | null {
|
|
273
273
|
if (this.#trustProxy) {
|
|
274
274
|
const forwarded = this.#raw.headers['x-forwarded-host']
|
|
275
|
-
|
|
275
|
+
// The first hop in the list, named rather than indexed: `split` always
|
|
276
|
+
// yields one element, and saying so beats being told it might not.
|
|
277
|
+
const [first] = forwarded?.split(',') ?? []
|
|
278
|
+
if (first !== undefined) return first.trim()
|
|
276
279
|
}
|
|
277
280
|
return this.#raw.headers.host ?? null
|
|
278
281
|
}
|
|
@@ -285,7 +288,8 @@ export class Request extends Macroable {
|
|
|
285
288
|
protocol(): string {
|
|
286
289
|
if (this.#trustProxy) {
|
|
287
290
|
const forwarded = this.#raw.headers['x-forwarded-proto']
|
|
288
|
-
|
|
291
|
+
const [first] = forwarded?.split(',') ?? []
|
|
292
|
+
if (first !== undefined) return first.trim().toLowerCase()
|
|
289
293
|
}
|
|
290
294
|
return this.#raw.scheme ?? 'http'
|
|
291
295
|
}
|
|
@@ -900,9 +904,8 @@ export class Request extends Macroable {
|
|
|
900
904
|
fieldName: string,
|
|
901
905
|
options?: import('../bodyparser/MultipartFile.js').FileValidationOptions,
|
|
902
906
|
): import('../bodyparser/MultipartFile.js').MultipartFile | null {
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
905
|
-
const file = files[0]
|
|
907
|
+
const [file] = this.#files.get(fieldName) ?? []
|
|
908
|
+
if (file === undefined) return null
|
|
906
909
|
if (options) file.validate(options)
|
|
907
910
|
return file
|
|
908
911
|
}
|
package/src/http/mime.ts
CHANGED
|
@@ -1317,7 +1317,8 @@ const CHARSETS: Record<string, string> = {
|
|
|
1317
1317
|
|
|
1318
1318
|
/** The charset for a type, or `undefined` when it carries none. */
|
|
1319
1319
|
function charsetFor(type: string): string | undefined {
|
|
1320
|
-
const
|
|
1320
|
+
const [head = ''] = type.split(';')
|
|
1321
|
+
const base = head.trim().toLowerCase()
|
|
1321
1322
|
const declared = CHARSETS[base]
|
|
1322
1323
|
if (declared !== undefined) return declared
|
|
1323
1324
|
return base.startsWith('text/') ? 'utf-8' : undefined
|
package/src/index.ts
CHANGED
|
@@ -254,7 +254,7 @@ export { ConsoleKernel, defineConfig, Ignitor, prettyPrintError } from './Ignito
|
|
|
254
254
|
// ─── Providers ──────────────────────────────────────────────
|
|
255
255
|
|
|
256
256
|
export { errors } from './errors/aggregate.js'
|
|
257
|
-
export type { AppContext, ProviderContract } from './Provider.js'
|
|
257
|
+
export type { AppContext, ConfigReader, ProviderContract } from './Provider.js'
|
|
258
258
|
export { Provider } from './Provider.js'
|
|
259
259
|
export type {
|
|
260
260
|
AuthenticatorName,
|
|
@@ -378,4 +378,4 @@ export {
|
|
|
378
378
|
StatsTracker,
|
|
379
379
|
} from './scheduler/index.js'
|
|
380
380
|
|
|
381
|
-
export const VERSION = '0.2.
|
|
381
|
+
export const VERSION = '0.2.16'
|
package/src/native/generated.ts
CHANGED
|
@@ -87,8 +87,14 @@ export declare class HyperServer {
|
|
|
87
87
|
* the safe default for a dev machine.
|
|
88
88
|
*/
|
|
89
89
|
constructor(port?: number | undefined | null, host?: string | undefined | null)
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Register the request handler.
|
|
92
|
+
*
|
|
93
|
+
* The request arrives as an OBJECT (`serde_json::Value` → `JsObject`) and
|
|
94
|
+
* the handler answers with a `NapiResponse` object — not the JSON strings
|
|
95
|
+
* this comment described until the boundary stopped carrying them.
|
|
96
|
+
*/
|
|
97
|
+
onRequest(callback: (request: Record<string, unknown>) => Promise<NapiResponse>): void
|
|
92
98
|
/**
|
|
93
99
|
* Configure the trusted-proxy CIDR list used by `request.ip` resolution.
|
|
94
100
|
* Must be called BEFORE `listen()`. Empty list = legacy permissive mode
|
|
@@ -155,12 +161,10 @@ export declare class HyperServer {
|
|
|
155
161
|
*/
|
|
156
162
|
writeStreamBytes(streamId: string, chunk: Uint8Array): Promise<boolean>
|
|
157
163
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
* HTTP/1.1 chunked encoding). Idempotent.
|
|
163
|
-
*/
|
|
164
|
+
* Close a stream from the server side. The matching response body
|
|
165
|
+
* finishes cleanly (hyper writes the final chunk + `0\r\n\r\n` for
|
|
166
|
+
* HTTP/1.1 chunked encoding). Idempotent.
|
|
167
|
+
*/
|
|
164
168
|
closeStream(streamId: string): Promise<boolean>
|
|
165
169
|
/**
|
|
166
170
|
* Install a one-shot callback fired the moment the matching stream
|
|
@@ -170,9 +170,9 @@ export class OpenApiGenerator {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (route.validators.length > 0 && ['post', 'put', 'patch'].includes(method)) {
|
|
173
|
-
const validatorName = route.validators
|
|
174
|
-
const schema = this.#runeSchemas.get(validatorName)
|
|
175
|
-
if (schema) {
|
|
173
|
+
const [validatorName] = route.validators
|
|
174
|
+
const schema = validatorName === undefined ? undefined : this.#runeSchemas.get(validatorName)
|
|
175
|
+
if (schema && validatorName !== undefined) {
|
|
176
176
|
operation.requestBody = {
|
|
177
177
|
required: true,
|
|
178
178
|
content: { 'application/json': { schema: this.#runeToJsonSchema(schema) } },
|
|
@@ -197,7 +197,9 @@ export class OpenApiGenerator {
|
|
|
197
197
|
/** Extract parameter names from path. */
|
|
198
198
|
#extractPathParams(path: string): string[] {
|
|
199
199
|
const matches = path.matchAll(/:(\w+)\??/g)
|
|
200
|
-
|
|
200
|
+
// Group 1 is required by the pattern, so every match carries it; the
|
|
201
|
+
// filter is how that is stated instead of asserted.
|
|
202
|
+
return [...matches].map((m) => m[1]).filter((name) => name !== undefined)
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
/** Generate a summary from route metadata. */
|
package/src/router/Router.ts
CHANGED
|
@@ -25,7 +25,16 @@ export interface SignedUrlOptions {
|
|
|
25
25
|
|
|
26
26
|
// ─── Types ──────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A route handler. Whatever it returns is awaited and dropped — the response
|
|
30
|
+
* is written through `ctx.response`.
|
|
31
|
+
*
|
|
32
|
+
* `unknown`, not `Promise<void> | void`: a bare `void` return type accepts a
|
|
33
|
+
* function returning anything, but a UNION containing it does not, so
|
|
34
|
+
* `router.get('/x', () => 'ok')` was a type error for returning a value the
|
|
35
|
+
* kernel already ignores.
|
|
36
|
+
*/
|
|
37
|
+
export type RouteHandlerFunction = (ctx: HttpContext) => unknown
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Controller tuple: [ControllerClass, 'methodName'].
|
|
@@ -267,7 +276,13 @@ export class RouteBuilder extends Macroable {
|
|
|
267
276
|
|
|
268
277
|
/** The primary definition — accessors read from it (all verbs share pattern/name). */
|
|
269
278
|
#primary(): RouteDefinition {
|
|
270
|
-
|
|
279
|
+
const [first] = this.#routes
|
|
280
|
+
if (first === undefined) {
|
|
281
|
+
// A route is constructed with at least one verb, so this cannot happen —
|
|
282
|
+
// and this is where that is stated rather than asserted past.
|
|
283
|
+
throw new Error('Route has no definitions')
|
|
284
|
+
}
|
|
285
|
+
return first
|
|
271
286
|
}
|
|
272
287
|
|
|
273
288
|
/** Name this route (for URL generation and redirects). */
|
|
@@ -1557,9 +1572,13 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1557
1572
|
const params: MatchedParams = {}
|
|
1558
1573
|
for (let i = 0; i < wildcardIdx; i++) {
|
|
1559
1574
|
const part = patternParts[i]
|
|
1575
|
+
const actual = actualParts[i]
|
|
1576
|
+
// Both are read out of arrays whose length was checked above; naming
|
|
1577
|
+
// them is what turns that check into something the compiler shares.
|
|
1578
|
+
if (part === undefined || actual === undefined) return null
|
|
1560
1579
|
if (part.startsWith(':')) {
|
|
1561
|
-
params[part.substring(1)] = safeDecodeURIComponent(
|
|
1562
|
-
} else if (part !==
|
|
1580
|
+
params[part.substring(1)] = safeDecodeURIComponent(actual)
|
|
1581
|
+
} else if (part !== actual) {
|
|
1563
1582
|
return null
|
|
1564
1583
|
}
|
|
1565
1584
|
}
|
|
@@ -1579,22 +1598,22 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1579
1598
|
|
|
1580
1599
|
const params: MatchedParams = {}
|
|
1581
1600
|
|
|
1582
|
-
for (
|
|
1583
|
-
const part = patternParts[i]
|
|
1601
|
+
for (const [i, part] of patternParts.entries()) {
|
|
1584
1602
|
const isOptional = part.endsWith('?')
|
|
1603
|
+
const actual = actualParts[i]
|
|
1585
1604
|
|
|
1586
1605
|
if (part.startsWith(':')) {
|
|
1587
1606
|
const paramName = isOptional ? part.slice(1, -1) : part.substring(1)
|
|
1588
|
-
if (
|
|
1607
|
+
if (actual !== undefined) {
|
|
1589
1608
|
// Percent-decode the captured segment (query strings already are) so
|
|
1590
1609
|
// `/users/Jos%C3%A9` yields `José` and matchers test the real value.
|
|
1591
1610
|
// safeDecode falls back to the raw segment on malformed input.
|
|
1592
|
-
params[paramName] = safeDecodeURIComponent(
|
|
1611
|
+
params[paramName] = safeDecodeURIComponent(actual)
|
|
1593
1612
|
}
|
|
1594
1613
|
// Optional param with no actual part — skip (param not set)
|
|
1595
|
-
} else if (
|
|
1614
|
+
} else if (actual !== undefined && part !== actual) {
|
|
1596
1615
|
return null
|
|
1597
|
-
} else if (
|
|
1616
|
+
} else if (actual === undefined && !isOptional) {
|
|
1598
1617
|
return null
|
|
1599
1618
|
}
|
|
1600
1619
|
}
|
|
@@ -1612,7 +1631,8 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1612
1631
|
* Comparing them verbatim sends that request to a fallback, or to a 404.
|
|
1613
1632
|
*/
|
|
1614
1633
|
function matchDomain(pattern: string, host: string): boolean {
|
|
1615
|
-
const
|
|
1634
|
+
const [hostname = ''] = host.split(':') // strip port
|
|
1635
|
+
const actualHost = hostname.toLowerCase()
|
|
1616
1636
|
const wanted = pattern.toLowerCase()
|
|
1617
1637
|
if (wanted === actualHost) return true
|
|
1618
1638
|
if (wanted.startsWith('*.')) {
|