@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.
Files changed (206) hide show
  1. package/README.md +1 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +6 -1
  4. package/dist/Application.js.map +1 -1
  5. package/dist/Ignitor.d.ts +9 -0
  6. package/dist/Ignitor.d.ts.map +1 -1
  7. package/dist/Ignitor.js +29 -3
  8. package/dist/Ignitor.js.map +1 -1
  9. package/dist/Provider.d.ts +20 -2
  10. package/dist/Provider.d.ts.map +1 -1
  11. package/dist/Provider.js.map +1 -1
  12. package/dist/bodyparser/BodyParserMiddleware.d.ts +1 -1
  13. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  14. package/dist/bodyparser/BodyParserMiddleware.js +12 -1
  15. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  16. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  17. package/dist/bodyparser/MultipartFile.js +7 -3
  18. package/dist/bodyparser/MultipartFile.js.map +1 -1
  19. package/dist/bodyparser/config.d.ts +15 -0
  20. package/dist/bodyparser/config.d.ts.map +1 -0
  21. package/dist/bodyparser/config.js +16 -0
  22. package/dist/bodyparser/config.js.map +1 -0
  23. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  24. package/dist/bodyparser/parseSize.js +3 -2
  25. package/dist/bodyparser/parseSize.js.map +1 -1
  26. package/dist/commands/MigrateBase.d.ts.map +1 -1
  27. package/dist/commands/MigrateBase.js +4 -1
  28. package/dist/commands/MigrateBase.js.map +1 -1
  29. package/dist/config/moduleConfig.d.ts +15 -0
  30. package/dist/config/moduleConfig.d.ts.map +1 -0
  31. package/dist/config/moduleConfig.js +24 -0
  32. package/dist/config/moduleConfig.js.map +1 -0
  33. package/dist/console/Kernel.d.ts.map +1 -1
  34. package/dist/console/Kernel.js +7 -0
  35. package/dist/console/Kernel.js.map +1 -1
  36. package/dist/console/cliui.d.ts +2 -0
  37. package/dist/console/cliui.d.ts.map +1 -1
  38. package/dist/console/cliui.js +2 -1
  39. package/dist/console/cliui.js.map +1 -1
  40. package/dist/console/types.d.ts +14 -0
  41. package/dist/console/types.d.ts.map +1 -1
  42. package/dist/console/types.js.map +1 -1
  43. package/dist/container/Container.d.ts.map +1 -1
  44. package/dist/container/Container.js +6 -1
  45. package/dist/container/Container.js.map +1 -1
  46. package/dist/container/types.d.ts +11 -1
  47. package/dist/container/types.d.ts.map +1 -1
  48. package/dist/decorators/Service.d.ts +1 -1
  49. package/dist/decorators/Service.d.ts.map +1 -1
  50. package/dist/env/Env.d.ts.map +1 -1
  51. package/dist/env/Env.js +4 -2
  52. package/dist/env/Env.js.map +1 -1
  53. package/dist/env/interpolate.d.ts.map +1 -1
  54. package/dist/env/interpolate.js +5 -3
  55. package/dist/env/interpolate.js.map +1 -1
  56. package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
  57. package/dist/errors/FuzzyMatcher.js +30 -13
  58. package/dist/errors/FuzzyMatcher.js.map +1 -1
  59. package/dist/errors/PipelineStageError.d.ts.map +1 -1
  60. package/dist/errors/PipelineStageError.js +4 -2
  61. package/dist/errors/PipelineStageError.js.map +1 -1
  62. package/dist/events/Emitter.d.ts +21 -4
  63. package/dist/events/Emitter.d.ts.map +1 -1
  64. package/dist/events/Emitter.js +6 -2
  65. package/dist/events/Emitter.js.map +1 -1
  66. package/dist/events/testing/FakeBus.d.ts +1 -1
  67. package/dist/events/testing/FakeBus.d.ts.map +1 -1
  68. package/dist/events/testing/FakeBus.js.map +1 -1
  69. package/dist/graphql/GraphQLEngine.d.ts +2 -2
  70. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  71. package/dist/graphql/GraphQLEngine.js +4 -2
  72. package/dist/graphql/GraphQLEngine.js.map +1 -1
  73. package/dist/helpers/bytes.d.ts.map +1 -1
  74. package/dist/helpers/bytes.js +13 -2
  75. package/dist/helpers/bytes.js.map +1 -1
  76. package/dist/helpers/string.js +3 -3
  77. package/dist/helpers/string.js.map +1 -1
  78. package/dist/http/Exception.d.ts +8 -4
  79. package/dist/http/Exception.d.ts.map +1 -1
  80. package/dist/http/Exception.js +14 -8
  81. package/dist/http/Exception.js.map +1 -1
  82. package/dist/http/Request.d.ts.map +1 -1
  83. package/dist/http/Request.js +10 -7
  84. package/dist/http/Request.js.map +1 -1
  85. package/dist/http/mime.d.ts.map +1 -1
  86. package/dist/http/mime.js +2 -1
  87. package/dist/http/mime.js.map +1 -1
  88. package/dist/index.d.ts +2 -2
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/native/generated.d.ts +12 -8
  92. package/dist/native/generated.d.ts.map +1 -1
  93. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
  94. package/dist/openapi/OpenApiGenerator.js +6 -4
  95. package/dist/openapi/OpenApiGenerator.js.map +1 -1
  96. package/dist/router/Router.d.ts +10 -1
  97. package/dist/router/Router.d.ts.map +1 -1
  98. package/dist/router/Router.js +22 -10
  99. package/dist/router/Router.js.map +1 -1
  100. package/dist/rpc/RpcRouter.d.ts +16 -3
  101. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  102. package/dist/rpc/RpcRouter.js +34 -3
  103. package/dist/rpc/RpcRouter.js.map +1 -1
  104. package/dist/scheduler/Schedule.d.ts +6 -1
  105. package/dist/scheduler/Schedule.d.ts.map +1 -1
  106. package/dist/scheduler/Schedule.js +5 -0
  107. package/dist/scheduler/Schedule.js.map +1 -1
  108. package/dist/scheduler/ScheduleProvider.d.ts +15 -11
  109. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  110. package/dist/scheduler/ScheduleProvider.js +21 -12
  111. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  112. package/dist/security/CookieSigner.d.ts.map +1 -1
  113. package/dist/security/CookieSigner.js +12 -5
  114. package/dist/security/CookieSigner.js.map +1 -1
  115. package/dist/security/SignedUrl.js +3 -2
  116. package/dist/security/SignedUrl.js.map +1 -1
  117. package/dist/session/SessionMiddleware.d.ts +2 -4
  118. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  119. package/dist/session/SessionMiddleware.js +13 -1
  120. package/dist/session/SessionMiddleware.js.map +1 -1
  121. package/dist/session/config.d.ts +25 -0
  122. package/dist/session/config.d.ts.map +1 -0
  123. package/dist/session/config.js +18 -0
  124. package/dist/session/config.js.map +1 -0
  125. package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
  126. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  127. package/dist/session/drivers/DatabaseDriver.js +23 -3
  128. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  129. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  130. package/dist/session/drivers/RedisDriver.js +10 -2
  131. package/dist/session/drivers/RedisDriver.js.map +1 -1
  132. package/dist/testing/ApiResponse.d.ts.map +1 -1
  133. package/dist/testing/ApiResponse.js +10 -11
  134. package/dist/testing/ApiResponse.js.map +1 -1
  135. package/dist/testing/TestClient.js +6 -6
  136. package/dist/testing/TestClient.js.map +1 -1
  137. package/dist/utils/Macroable.d.ts +6 -2
  138. package/dist/utils/Macroable.d.ts.map +1 -1
  139. package/dist/utils/Macroable.js +4 -0
  140. package/dist/utils/Macroable.js.map +1 -1
  141. package/dist/utils/objectPath.d.ts.map +1 -1
  142. package/dist/utils/objectPath.js +14 -6
  143. package/dist/utils/objectPath.js.map +1 -1
  144. package/events.darwin-arm64.node +0 -0
  145. package/events.darwin-x64.node +0 -0
  146. package/events.linux-arm64-gnu.node +0 -0
  147. package/events.linux-x64-gnu.node +0 -0
  148. package/events.win32-x64-msvc.node +0 -0
  149. package/index.darwin-arm64.node +0 -0
  150. package/index.darwin-x64.node +0 -0
  151. package/index.linux-arm64-gnu.node +0 -0
  152. package/index.linux-x64-gnu.node +0 -0
  153. package/index.win32-x64-msvc.node +0 -0
  154. package/package.json +13 -4
  155. package/scheduler.darwin-arm64.node +0 -0
  156. package/scheduler.darwin-x64.node +0 -0
  157. package/scheduler.linux-arm64-gnu.node +0 -0
  158. package/scheduler.linux-x64-gnu.node +0 -0
  159. package/scheduler.win32-x64-msvc.node +0 -0
  160. package/scripts/build-napi-types.mjs +4 -3
  161. package/scripts/generate-napi-types.mjs +28 -12
  162. package/src/Application.ts +6 -1
  163. package/src/Ignitor.ts +42 -3
  164. package/src/Provider.ts +21 -2
  165. package/src/bodyparser/BodyParserMiddleware.ts +12 -1
  166. package/src/bodyparser/MultipartFile.ts +7 -3
  167. package/src/bodyparser/config.ts +18 -0
  168. package/src/bodyparser/parseSize.ts +3 -2
  169. package/src/commands/MigrateBase.ts +4 -1
  170. package/src/config/moduleConfig.ts +24 -0
  171. package/src/console/BaseCommand.ts +1 -1
  172. package/src/console/Kernel.ts +8 -0
  173. package/src/console/cliui.ts +2 -1
  174. package/src/console/types.ts +15 -0
  175. package/src/container/Container.ts +15 -6
  176. package/src/container/types.ts +11 -1
  177. package/src/decorators/Service.ts +1 -1
  178. package/src/env/Env.ts +4 -2
  179. package/src/env/interpolate.ts +5 -3
  180. package/src/errors/FuzzyMatcher.ts +34 -13
  181. package/src/errors/PipelineStageError.ts +4 -2
  182. package/src/events/Emitter.ts +34 -8
  183. package/src/events/testing/FakeBus.ts +12 -5
  184. package/src/graphql/GraphQLEngine.ts +11 -7
  185. package/src/helpers/bytes.ts +13 -2
  186. package/src/helpers/string.ts +3 -3
  187. package/src/http/Exception.ts +14 -8
  188. package/src/http/Request.ts +8 -5
  189. package/src/http/mime.ts +2 -1
  190. package/src/index.ts +2 -2
  191. package/src/native/generated.ts +12 -8
  192. package/src/openapi/OpenApiGenerator.ts +6 -4
  193. package/src/router/Router.ts +31 -11
  194. package/src/rpc/RpcRouter.ts +38 -8
  195. package/src/scheduler/Schedule.ts +8 -1
  196. package/src/scheduler/ScheduleProvider.ts +22 -12
  197. package/src/security/CookieSigner.ts +15 -5
  198. package/src/security/SignedUrl.ts +3 -2
  199. package/src/session/SessionMiddleware.ts +15 -2
  200. package/src/session/config.ts +29 -0
  201. package/src/session/drivers/DatabaseDriver.ts +36 -8
  202. package/src/session/drivers/RedisDriver.ts +13 -5
  203. package/src/testing/ApiResponse.ts +9 -10
  204. package/src/testing/TestClient.ts +6 -6
  205. package/src/utils/Macroable.ts +6 -2
  206. 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
- for (const key of Object.keys(schema)) {
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] = schema[key].validate(key, values[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
  }
@@ -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
- if (prefixed) {
50
- const resolver = identifiers.get(prefixed[1])
51
- if (resolver) return resolver(prefixed[2])
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
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0))
18
-
19
- for (let i = 0; i <= m; i++) dp[i][0] = i
20
- for (let j = 0; j <= n; j++) dp[0][j] = j
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
- dp[i][j] = Math.min(
26
- dp[i - 1][j] + 1, // deletion
27
- dp[i][j - 1] + 1, // insertion
28
- dp[i - 1][j - 1] + cost, // substitution
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 dp[m][n]
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 matches = findClosestMatches(input, candidates)
85
- if (matches.length === 0) return ''
104
+ const [best, ...rest] = findClosestMatches(input, candidates)
105
+ if (best === undefined) return ''
86
106
 
87
- if (matches.length === 1) {
88
- return `Did you mean '${matches[0].candidate}'?`
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
- const stageName = PIPELINE_STAGES[clampedIndex]
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 = clampedIndex + 1 < total ? PIPELINE_STAGES[clampedIndex + 1] : '(end)'
49
+ const nextStage = PIPELINE_STAGES[clampedIndex + 1] ?? '(end)'
48
50
 
49
51
  const errorCode = originalError instanceof ReamError ? originalError.code : 'E_PIPELINE_ERROR'
50
52
 
@@ -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): Promise<void> | void
26
+ handle(event: T): unknown
27
27
  }
28
28
 
29
29
  type EventConstructor<T = unknown> = new (...args: never[]) => T
30
30
 
31
- type ListenerFn<T = unknown> = (event: T) => Promise<void> | void
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
- type ListenerConstructor<T = unknown> = new (...args: never[]) => ListenerClass<T>
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
- make<T>(target: new (...args: never[]) => T): Promise<T>
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 as ListenerConstructor<T>)
331
- : new (listener as ListenerConstructor<T>)()
332
- await instance.handle(event)
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): boolean {
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
- /** Subscribers may return `void` (sync) or `PromiseLike<void>` (async).
45
- * Async rejections are logged via `console.error` to keep them from
46
- * escaping to `unhandledRejection`. */
47
- callback: (eventJson: string) => void | PromiseLike<void>
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) => void | PromiseLike<void>): number {
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: unknown[]) => Record<string, (...args: unknown[]) => unknown>
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: unknown[]) => T): Promise<T> }
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: { make<T>(target: new (...args: unknown[]) => T): Promise<T> }): void {
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: unknown[]) => Record<string, (...args: unknown[]) => unknown>,
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
- const instance = this.#container
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
- : new entry.handlerClass()
378
+ : Reflect.construct(entry.handlerClass, [])
375
379
 
376
- const handler = instance[entry.methodName]
380
+ const handler = Reflect.get(instance, entry.methodName)
377
381
  if (typeof handler !== 'function') {
378
382
  return {
379
383
  ok: false,
@@ -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
- const amount = matched ? Number.parseFloat(matched[1]) : Number.parseInt(value, 10)
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 = (matched ? matched[2].toLowerCase() : 'b') as 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
 
@@ -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 words = splitWords(s)
218
- if (words.length === 0) return ''
219
- return [words[0]?.charAt(0).toUpperCase() + words[0]?.slice(1), ...words.slice(1)].join(' ')
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
  // ---------------------------------------------------------------------------
@@ -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
- for (const range of Object.keys(this.statusPages)) {
319
- Object.assign(expanded, parseStatusRange(range, this.statusPages[range]))
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
- * DEVIATION (named): ream lists `json` FIRST, so a request with no or `*` /`*`
444
- * Accept defaults to JSON (API-first). AdonisJS lists `html` first
445
- * (full-stack default). Explicit `text/html` / `vnd.api+json` are honoured
446
- * identically.
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(['json', 'html', 'application/vnd.api+json'])) {
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(['json', 'html', 'application/vnd.api+json'])) {
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':
@@ -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
- if (forwarded) return forwarded.split(',')[0].trim()
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
- if (forwarded) return forwarded.split(',')[0].trim().toLowerCase()
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 files = this.#files.get(fieldName)
904
- if (!files || files.length === 0) return null
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 base = type.split(';')[0].trim().toLowerCase()
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.14'
381
+ export const VERSION = '0.2.16'
@@ -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
- /** Register the request handler. Callback receives JSON request string, must return JSON response string. */
91
- onRequest(callback: (request: string) => Promise<string> | string): void
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
- * Close a stream from the server side. The matching response body
159
- * finishes cleanly (hyper writes the final chunk + `0
160
-
161
- ` for
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[0]
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
- return [...matches].map((m) => m[1])
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. */
@@ -25,7 +25,16 @@ export interface SignedUrlOptions {
25
25
 
26
26
  // ─── Types ──────────────────────────────────────────────────
27
27
 
28
- export type RouteHandlerFunction = (ctx: HttpContext) => Promise<void> | void
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
- return this.#routes[0]
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(actualParts[i])
1562
- } else if (part !== actualParts[i]) {
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 (let i = 0; i < patternParts.length; i++) {
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 (i < actualParts.length) {
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(actualParts[i])
1611
+ params[paramName] = safeDecodeURIComponent(actual)
1593
1612
  }
1594
1613
  // Optional param with no actual part — skip (param not set)
1595
- } else if (i < actualParts.length && part !== actualParts[i]) {
1614
+ } else if (actual !== undefined && part !== actual) {
1596
1615
  return null
1597
- } else if (i >= actualParts.length && !isOptional) {
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 actualHost = host.split(':')[0].toLowerCase() // strip port
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('*.')) {