@c9up/ream 0.2.14 → 0.2.15
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/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.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/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/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/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/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/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 +3 -3
- 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/Provider.ts +21 -2
- package/src/bodyparser/MultipartFile.ts +7 -3
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- 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/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- 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
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
export function parseSize(size: string): number {
|
|
7
7
|
const match = size.match(/^(\d+)(kb|mb|gb)?$/i)
|
|
8
8
|
if (!match) return 1024 * 1024 // default 1mb
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const [, digits, unit] = match
|
|
10
|
+
const num = parseInt(digits ?? '0', 10)
|
|
11
|
+
switch (unit?.toLowerCase()) {
|
|
11
12
|
case 'kb':
|
|
12
13
|
return num * 1024
|
|
13
14
|
case 'mb':
|
|
@@ -19,7 +19,10 @@ import type { MigrationRegistry } from '../migrations/MigrationRegistry.js'
|
|
|
19
19
|
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
20
20
|
|
|
21
21
|
export abstract class MigrateBase extends BaseCommand {
|
|
22
|
-
|
|
22
|
+
// `drivesMigrations` because these three ARE the migration run: without it the
|
|
23
|
+
// boot applies everything first, so `migrate` says "nothing to migrate" after
|
|
24
|
+
// the work is done, and `migrate:status` mutates the schema it reports on.
|
|
25
|
+
static override options: CommandOptions = { startApp: true, drivesMigrations: true }
|
|
23
26
|
|
|
24
27
|
@flags.string({
|
|
25
28
|
description: 'Only this migration source, by the name its package registered',
|
|
@@ -117,7 +117,7 @@ export abstract class BaseCommand {
|
|
|
117
117
|
error?: unknown
|
|
118
118
|
|
|
119
119
|
/** Assigned by the kernel — see {@link terminate}. */
|
|
120
|
-
protected
|
|
120
|
+
declare protected onTerminate?: () => void | Promise<void>
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Lifecycle hooks, run in this order around `run()`: `prepare`, `interact`,
|
package/src/console/Kernel.ts
CHANGED
|
@@ -999,6 +999,14 @@ export class Kernel {
|
|
|
999
999
|
)
|
|
1000
1000
|
}
|
|
1001
1001
|
|
|
1002
|
+
// Before the boot, not after: a data package that migrates on boot for the
|
|
1003
|
+
// convenience of `dev` would otherwise apply everything a moment before the
|
|
1004
|
+
// command that exists to apply it runs — leaving `migrate` with nothing to
|
|
1005
|
+
// report and `migrate:status` having changed what it was asked to look at.
|
|
1006
|
+
if (Command.options?.drivesMigrations === true) {
|
|
1007
|
+
process.env.REAM_SKIP_BOOT_MIGRATE = '1'
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1002
1010
|
return this.#startApp()
|
|
1003
1011
|
}
|
|
1004
1012
|
|
package/src/console/types.ts
CHANGED
|
@@ -37,6 +37,21 @@ export interface CommandOptions {
|
|
|
37
37
|
* default so a typo (`--warm` for `--warn`) is reported, not swallowed.
|
|
38
38
|
*/
|
|
39
39
|
allowUnknownFlags?: boolean
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This command drives migrations itself, so booting must not run them.
|
|
43
|
+
*
|
|
44
|
+
* A data package may migrate on boot for the convenience of `dev` — atlas
|
|
45
|
+
* does, outside production. For the commands that exist to migrate, that
|
|
46
|
+
* convenience is the bug: the boot applies everything, and then `migrate`
|
|
47
|
+
* reports "nothing to migrate" while `migrate:status`, asked only to look,
|
|
48
|
+
* has changed the schema it is reporting on.
|
|
49
|
+
*
|
|
50
|
+
* Declaring this sets `REAM_SKIP_BOOT_MIGRATE=1` before the application
|
|
51
|
+
* starts, which is the flag a data package watches for. Any package's own
|
|
52
|
+
* migration command opts in the same way; the framework names none of them.
|
|
53
|
+
*/
|
|
54
|
+
drivesMigrations?: boolean
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/** Metadata recorded by an `@args.*` decorator. */
|
|
@@ -63,7 +63,7 @@ interface ResolutionChain {
|
|
|
63
63
|
* down. Saved and restored around each construction, so a dependency's own
|
|
64
64
|
* dependencies see IT as their parent, not its parent.
|
|
65
65
|
*/
|
|
66
|
-
parent?: new (
|
|
66
|
+
parent?: abstract new (
|
|
67
67
|
...args: never[]
|
|
68
68
|
) => unknown
|
|
69
69
|
}
|
|
@@ -91,8 +91,12 @@ export class Container {
|
|
|
91
91
|
/** Alias key → the token it forwards to (AdonisJS `container.alias`). */
|
|
92
92
|
#aliases: Map<ServiceToken, ServiceToken> = new Map()
|
|
93
93
|
/** parent class → (binding key → the factory that class gets instead). */
|
|
94
|
-
#contextualBindings: Map<
|
|
95
|
-
new
|
|
94
|
+
#contextualBindings: Map<
|
|
95
|
+
abstract new (
|
|
96
|
+
...args: never[]
|
|
97
|
+
) => unknown,
|
|
98
|
+
Map<ServiceToken, ServiceFactory>
|
|
99
|
+
> = new Map()
|
|
96
100
|
/** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
|
|
97
101
|
#resolvingHooks: Map<ServiceToken, Array<(value: unknown) => void | Promise<void>>> = new Map()
|
|
98
102
|
/**
|
|
@@ -625,7 +629,7 @@ export class Container {
|
|
|
625
629
|
* 3. No params → plain `new Class()`
|
|
626
630
|
*/
|
|
627
631
|
async #autoConstruct(
|
|
628
|
-
target: new (...args: never[]) => unknown,
|
|
632
|
+
target: abstract new (...args: never[]) => unknown,
|
|
629
633
|
runtimeValues?: unknown[],
|
|
630
634
|
): Promise<unknown> {
|
|
631
635
|
// Everything resolved inside is a dependency OF `target`, which is what a
|
|
@@ -642,7 +646,7 @@ export class Container {
|
|
|
642
646
|
}
|
|
643
647
|
|
|
644
648
|
async #constructWithDeps(
|
|
645
|
-
target: new (...args: never[]) => unknown,
|
|
649
|
+
target: abstract new (...args: never[]) => unknown,
|
|
646
650
|
runtimeValues?: unknown[],
|
|
647
651
|
): Promise<unknown> {
|
|
648
652
|
const metadata = getServiceMetadata(target)
|
|
@@ -719,7 +723,12 @@ export class Container {
|
|
|
719
723
|
)
|
|
720
724
|
}
|
|
721
725
|
// (d) Genuine zero-argument class.
|
|
722
|
-
|
|
726
|
+
//
|
|
727
|
+
// `Reflect.construct`, not `new target()`: a token may be an abstract
|
|
728
|
+
// class (that is what a contextual binding binds against), and `new` on
|
|
729
|
+
// one is a compile error even though the runtime constructs it happily —
|
|
730
|
+
// `abstract` exists only at compile time.
|
|
731
|
+
const instance = Reflect.construct(target, [])
|
|
723
732
|
this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
|
|
724
733
|
return instance
|
|
725
734
|
}
|
package/src/container/types.ts
CHANGED
|
@@ -16,7 +16,17 @@ export interface ServiceMetadata {
|
|
|
16
16
|
/** Token used to resolve a service — class constructor, string name, or registered symbol. */
|
|
17
17
|
// `never[]` accepts every constructor shape — parameters are contravariant, so
|
|
18
18
|
// a rest of `never` is assignable from any concrete list.
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* What the container accepts as a token.
|
|
21
|
+
*
|
|
22
|
+
* `abstract new`, not `new`: binding an implementation against an abstract base
|
|
23
|
+
* is the whole point of a contextual binding — `@Inject(Hash)` where `Hash` is
|
|
24
|
+
* the abstract class and `Bcrypt` is what the container provides. An abstract
|
|
25
|
+
* class has no construct signature, so spelt `new (...)` the type rejected
|
|
26
|
+
* exactly the pattern the feature exists for. `abstract new` admits concrete
|
|
27
|
+
* classes too.
|
|
28
|
+
*/
|
|
29
|
+
export type ServiceToken = (abstract new (...args: never[]) => unknown) | string | symbol
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* What a binding factory is handed so it can resolve its own dependencies
|
|
@@ -14,7 +14,7 @@ const INJECT_METADATA_KEY = Symbol('ream:inject')
|
|
|
14
14
|
// `never[]` accepts every constructor shape: parameters are contravariant, so a
|
|
15
15
|
// rest of `never` is assignable from any concrete list. Same spelling as the
|
|
16
16
|
// router's, so a class flows between the two without an assertion.
|
|
17
|
-
type AnyConstructor = new (...args: never[]) => unknown
|
|
17
|
+
type AnyConstructor = abstract new (...args: never[]) => unknown
|
|
18
18
|
|
|
19
19
|
/** Registry of all decorated services. */
|
|
20
20
|
const serviceRegistry: Map<AnyConstructor, ServiceMetadata> = new Map()
|
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.15'
|
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
|