@c9up/ream 0.2.1 → 0.2.3

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 (163) hide show
  1. package/dist/Ignitor.d.ts.map +1 -1
  2. package/dist/Ignitor.js +15 -1
  3. package/dist/Ignitor.js.map +1 -1
  4. package/dist/container/Container.d.ts +18 -0
  5. package/dist/container/Container.d.ts.map +1 -1
  6. package/dist/container/Container.js +29 -0
  7. package/dist/container/Container.js.map +1 -1
  8. package/dist/env/Env.d.ts +11 -0
  9. package/dist/env/Env.d.ts.map +1 -1
  10. package/dist/env/Env.js +33 -17
  11. package/dist/env/Env.js.map +1 -1
  12. package/dist/env/Secret.d.ts +17 -0
  13. package/dist/env/Secret.d.ts.map +1 -1
  14. package/dist/env/Secret.js +23 -0
  15. package/dist/env/Secret.js.map +1 -1
  16. package/dist/errors/aggregate.d.ts +27 -0
  17. package/dist/errors/aggregate.d.ts.map +1 -0
  18. package/dist/errors/aggregate.js +27 -0
  19. package/dist/errors/aggregate.js.map +1 -0
  20. package/dist/events/Emitter.d.ts +18 -1
  21. package/dist/events/Emitter.d.ts.map +1 -1
  22. package/dist/events/Emitter.js +25 -6
  23. package/dist/events/Emitter.js.map +1 -1
  24. package/dist/events/services/main.d.ts.map +1 -1
  25. package/dist/events/services/main.js +3 -10
  26. package/dist/events/services/main.js.map +1 -1
  27. package/dist/helpers/MessageBuilder.d.ts +29 -0
  28. package/dist/helpers/MessageBuilder.d.ts.map +1 -0
  29. package/dist/helpers/MessageBuilder.js +63 -0
  30. package/dist/helpers/MessageBuilder.js.map +1 -0
  31. package/dist/helpers/VerificationToken.d.ts +56 -0
  32. package/dist/helpers/VerificationToken.d.ts.map +1 -0
  33. package/dist/helpers/VerificationToken.js +70 -0
  34. package/dist/helpers/VerificationToken.js.map +1 -0
  35. package/dist/helpers/defineStaticProperty.d.ts +15 -0
  36. package/dist/helpers/defineStaticProperty.d.ts.map +1 -0
  37. package/dist/helpers/defineStaticProperty.js +47 -0
  38. package/dist/helpers/defineStaticProperty.js.map +1 -0
  39. package/dist/helpers/duration.d.ts +20 -0
  40. package/dist/helpers/duration.d.ts.map +1 -1
  41. package/dist/helpers/duration.js +71 -0
  42. package/dist/helpers/duration.js.map +1 -1
  43. package/dist/helpers/fs.d.ts +39 -0
  44. package/dist/helpers/fs.d.ts.map +1 -0
  45. package/dist/helpers/fs.js +70 -0
  46. package/dist/helpers/fs.js.map +1 -0
  47. package/dist/helpers/index.d.ts +8 -0
  48. package/dist/helpers/index.d.ts.map +1 -1
  49. package/dist/helpers/index.js +7 -0
  50. package/dist/helpers/index.js.map +1 -1
  51. package/dist/helpers/safeTiming.d.ts +14 -0
  52. package/dist/helpers/safeTiming.d.ts.map +1 -0
  53. package/dist/helpers/safeTiming.js +42 -0
  54. package/dist/helpers/safeTiming.js.map +1 -0
  55. package/dist/helpers/string.d.ts +85 -0
  56. package/dist/helpers/string.d.ts.map +1 -1
  57. package/dist/helpers/string.js +127 -1
  58. package/dist/helpers/string.js.map +1 -1
  59. package/dist/http/Exception.d.ts +76 -6
  60. package/dist/http/Exception.d.ts.map +1 -1
  61. package/dist/http/Exception.js +178 -85
  62. package/dist/http/Exception.js.map +1 -1
  63. package/dist/http/HttpContext.d.ts +14 -1
  64. package/dist/http/HttpContext.d.ts.map +1 -1
  65. package/dist/http/HttpContext.js.map +1 -1
  66. package/dist/http/RedirectBuilder.d.ts +16 -0
  67. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  68. package/dist/http/RedirectBuilder.js +34 -9
  69. package/dist/http/RedirectBuilder.js.map +1 -1
  70. package/dist/http/Request.d.ts.map +1 -1
  71. package/dist/http/Request.js +5 -17
  72. package/dist/http/Request.js.map +1 -1
  73. package/dist/index.d.ts +11 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +9 -4
  76. package/dist/index.js.map +1 -1
  77. package/dist/router/Router.d.ts +54 -0
  78. package/dist/router/Router.d.ts.map +1 -1
  79. package/dist/router/Router.js +73 -4
  80. package/dist/router/Router.js.map +1 -1
  81. package/dist/services/app.d.ts.map +1 -1
  82. package/dist/services/app.js +3 -21
  83. package/dist/services/app.js.map +1 -1
  84. package/dist/services/console.d.ts.map +1 -1
  85. package/dist/services/console.js +3 -19
  86. package/dist/services/console.js.map +1 -1
  87. package/dist/services/createServiceProxy.d.ts +19 -0
  88. package/dist/services/createServiceProxy.d.ts.map +1 -0
  89. package/dist/services/createServiceProxy.js +44 -0
  90. package/dist/services/createServiceProxy.js.map +1 -0
  91. package/dist/services/router.d.ts.map +1 -1
  92. package/dist/services/router.js +3 -12
  93. package/dist/services/router.js.map +1 -1
  94. package/dist/services/server.d.ts.map +1 -1
  95. package/dist/services/server.js +3 -22
  96. package/dist/services/server.js.map +1 -1
  97. package/dist/session/ReadOnlyValuesStore.d.ts +11 -0
  98. package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -1
  99. package/dist/session/ReadOnlyValuesStore.js +17 -0
  100. package/dist/session/ReadOnlyValuesStore.js.map +1 -1
  101. package/dist/session/Session.d.ts +11 -3
  102. package/dist/session/Session.d.ts.map +1 -1
  103. package/dist/session/Session.js +15 -6
  104. package/dist/session/Session.js.map +1 -1
  105. package/dist/session/SessionMiddleware.js +2 -1
  106. package/dist/session/SessionMiddleware.js.map +1 -1
  107. package/dist/session/errors.d.ts +25 -0
  108. package/dist/session/errors.d.ts.map +1 -0
  109. package/dist/session/errors.js +32 -0
  110. package/dist/session/errors.js.map +1 -0
  111. package/dist/types/authenticators.d.ts +24 -0
  112. package/dist/types/authenticators.d.ts.map +1 -0
  113. package/dist/types/authenticators.js +2 -0
  114. package/dist/types/authenticators.js.map +1 -0
  115. package/dist/types/container.d.ts +64 -0
  116. package/dist/types/container.d.ts.map +1 -0
  117. package/dist/types/container.js +23 -0
  118. package/dist/types/container.js.map +1 -0
  119. package/dist/types/index.d.ts +10 -0
  120. package/dist/types/index.d.ts.map +1 -0
  121. package/dist/types/index.js +9 -0
  122. package/dist/types/index.js.map +1 -0
  123. package/events.darwin-arm64.node +0 -0
  124. package/events.darwin-x64.node +0 -0
  125. package/events.linux-arm64-gnu.node +0 -0
  126. package/events.linux-x64-gnu.node +0 -0
  127. package/events.win32-x64-msvc.node +0 -0
  128. package/index.win32-x64-msvc.node +0 -0
  129. package/package.json +13 -1
  130. package/scheduler.win32-x64-msvc.node +0 -0
  131. package/src/Ignitor.ts +15 -1
  132. package/src/container/Container.ts +31 -0
  133. package/src/env/Env.ts +34 -15
  134. package/src/env/Secret.ts +26 -0
  135. package/src/errors/aggregate.ts +36 -0
  136. package/src/events/Emitter.ts +30 -5
  137. package/src/events/services/main.ts +6 -12
  138. package/src/helpers/MessageBuilder.ts +69 -0
  139. package/src/helpers/VerificationToken.ts +93 -0
  140. package/src/helpers/defineStaticProperty.ts +51 -0
  141. package/src/helpers/duration.ts +75 -0
  142. package/src/helpers/fs.ts +104 -0
  143. package/src/helpers/index.ts +8 -0
  144. package/src/helpers/safeTiming.ts +45 -0
  145. package/src/helpers/string.ts +145 -1
  146. package/src/http/Exception.ts +225 -99
  147. package/src/http/HttpContext.ts +14 -1
  148. package/src/http/RedirectBuilder.ts +38 -8
  149. package/src/http/Request.ts +5 -16
  150. package/src/index.ts +24 -2
  151. package/src/router/Router.ts +87 -4
  152. package/src/services/app.ts +6 -23
  153. package/src/services/console.ts +6 -21
  154. package/src/services/createServiceProxy.ts +47 -0
  155. package/src/services/router.ts +6 -14
  156. package/src/services/server.ts +6 -24
  157. package/src/session/ReadOnlyValuesStore.ts +20 -0
  158. package/src/session/Session.ts +16 -11
  159. package/src/session/SessionMiddleware.ts +2 -1
  160. package/src/session/errors.ts +39 -0
  161. package/src/types/authenticators.ts +24 -0
  162. package/src/types/container.ts +66 -0
  163. package/src/types/index.ts +10 -0
@@ -7,6 +7,7 @@
7
7
  * @implements FR21
8
8
  */
9
9
 
10
+ import { parseQueryString } from '../bodyparser/qsParse.js'
10
11
  import type { CookieSigner } from '../security/CookieSigner.js'
11
12
  import type { SignedUrl } from '../security/SignedUrl.js'
12
13
  import type { Dict } from '../types/helpers.js'
@@ -701,6 +702,10 @@ export class Request extends Macroable {
701
702
  /** Get parsed query string as an object. */
702
703
  qs(): Dict<unknown> {
703
704
  if (!this.#parsedQs) {
705
+ // The same parser the body parser uses. A local one lived here and split
706
+ // on "&" only: `?filter[status]=open` stayed the literal key
707
+ // "filter[status]", and `?tags[]=a&tags[]=b` silently kept only "b" —
708
+ // both are ordinary shapes for a filter or an HTML multi-select.
704
709
  this.#parsedQs = parseQueryString(this.#raw.query)
705
710
  }
706
711
  return { ...this.#parsedQs }
@@ -1012,22 +1017,6 @@ function safeDecode(value: string): string {
1012
1017
  }
1013
1018
  }
1014
1019
 
1015
- function parseQueryString(qs: string): Dict<unknown> {
1016
- if (!qs) return {}
1017
- const result: Dict<unknown> = {}
1018
- for (const pair of qs.split('&')) {
1019
- const eqIdx = pair.indexOf('=')
1020
- if (eqIdx === -1) {
1021
- result[safeDecode(pair)] = ''
1022
- } else {
1023
- const key = safeDecode(pair.slice(0, eqIdx))
1024
- const value = safeDecode(pair.slice(eqIdx + 1))
1025
- result[key] = value
1026
- }
1027
- }
1028
- return result
1029
- }
1030
-
1031
1020
  interface AcceptEntry {
1032
1021
  value: string
1033
1022
  q: number
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  createError,
41
41
  E_FORBIDDEN,
42
42
  E_HTTP_EXCEPTION,
43
+ E_HTTP_REQUEST_ABORTED,
43
44
  E_ROUTE_NOT_FOUND,
44
45
  E_ROW_NOT_FOUND,
45
46
  E_UNAUTHORIZED,
@@ -48,6 +49,7 @@ export {
48
49
  type ExceptionClass,
49
50
  type ExceptionConstructor,
50
51
  ExceptionHandler,
52
+ type HttpError,
51
53
  InvalidArgumentsException,
52
54
  RuntimeException,
53
55
  type StatusPageRenderer,
@@ -58,6 +60,8 @@ export { RedirectBuilder } from './http/RedirectBuilder.js'
58
60
  export type { RawRequest } from './http/Request.js'
59
61
  export { Request } from './http/Request.js'
60
62
  export { Response } from './http/Response.js'
63
+ export type { SseStreamOptions } from './http/SseStream.js'
64
+ export { SseStream } from './http/SseStream.js'
61
65
  export { type GetterFn, Macroable, type MacroFn } from './utils/Macroable.js'
62
66
 
63
67
  // ─── Session ────────────────────────────────────────────────
@@ -77,6 +81,12 @@ export type {
77
81
  SessionRedisClientSource,
78
82
  } from './session/drivers/RedisDriver.js'
79
83
  export { RedisDriver as SessionRedisDriver } from './session/drivers/RedisDriver.js'
84
+ export {
85
+ E_SESSION_NOT_READY,
86
+ E_SESSION_TAGGING_NOT_SUPPORTED,
87
+ } from './session/errors.js'
88
+ export type { ValuePath } from './session/ReadOnlyValuesStore.js'
89
+ export { ReadOnlyValuesStore } from './session/ReadOnlyValuesStore.js'
80
90
  export type {
81
91
  SessionConfig,
82
92
  SessionDriver,
@@ -139,7 +149,11 @@ export { StaticProvider } from './storage/StaticProvider.js'
139
149
 
140
150
  // Request-filter security (CSRF, XSS, rate-limit, CORS, headers, path/param)
141
151
  // lives in @c9up/blackhole. ream core keeps only the crypto primitives.
142
- export { CookieSigner } from './security/CookieSigner.js'
152
+ export {
153
+ CookieSigner,
154
+ E_INSECURE_APP_KEY,
155
+ E_MISSING_APP_KEY,
156
+ } from './security/CookieSigner.js'
143
157
  export {
144
158
  constantTimeEq,
145
159
  hasNativeCrypto,
@@ -179,6 +193,7 @@ export {
179
193
  matchers,
180
194
  OnRouteBuilder,
181
195
  RouteBuilder,
196
+ RouteResource,
182
197
  Router,
183
198
  UrlBuilder,
184
199
  } from './router/Router.js'
@@ -238,8 +253,15 @@ export { ConsoleKernel, defineConfig, Ignitor, prettyPrintError } from './Ignito
238
253
 
239
254
  // ─── Providers ──────────────────────────────────────────────
240
255
 
256
+ export { errors } from './errors/aggregate.js'
241
257
  export type { AppContext, ProviderContract } from './Provider.js'
242
258
  export { Provider } from './Provider.js'
259
+ export type {
260
+ AuthenticatorName,
261
+ Authenticators,
262
+ ContainerBinding,
263
+ ContainerBindings,
264
+ } from './types/index.js'
243
265
 
244
266
  // ─── Codemods ───────────────────────────────────────────────
245
267
 
@@ -349,4 +371,4 @@ export {
349
371
  StatsTracker,
350
372
  } from './scheduler/index.js'
351
373
 
352
- export const VERSION = '0.1.7'
374
+ export const VERSION = '0.2.3'
@@ -402,6 +402,54 @@ export class RouteBuilder extends Macroable {
402
402
  getDefinition(): RouteDefinition {
403
403
  return this.#primary()
404
404
  }
405
+
406
+ /**
407
+ * The handler this route dispatches to (AdonisJS `route.getHandler`).
408
+ *
409
+ * Either the inline function or the controller/method pair — a route always
410
+ * has exactly one of the two.
411
+ */
412
+ getHandler(): RouteHandlerFunction | { target: AnyConstructor; method: string } | null {
413
+ const route = this.#primary()
414
+ return route.controller ?? route.handler
415
+ }
416
+
417
+ /**
418
+ * Every middleware attached to this route (AdonisJS `route.getMiddleware`).
419
+ *
420
+ * Named entries and inline functions in one list, in the order they run.
421
+ */
422
+ getMiddleware(): Array<string | MiddlewareFunction> {
423
+ const route = this.#primary()
424
+ return [...route.middleware, ...route.inlineMiddleware]
425
+ }
426
+
427
+ /**
428
+ * A serialisable description of this route (AdonisJS `route.toJSON`).
429
+ *
430
+ * What a routes-list command or a route-manifest generator reads, and why it
431
+ * carries names rather than the functions themselves.
432
+ */
433
+ toJSON(): {
434
+ name?: string
435
+ pattern: string
436
+ methods: string[]
437
+ handler: RouteHandlerFunction | { target: AnyConstructor; method: string } | null
438
+ middleware: string[]
439
+ domain?: string
440
+ matchers: Record<string, ParamMatcher>
441
+ } {
442
+ const route = this.#primary()
443
+ return {
444
+ ...(route.name === undefined ? {} : { name: route.name }),
445
+ pattern: route.path,
446
+ methods: this.#routes.map((entry) => entry.method),
447
+ handler: this.getHandler(),
448
+ middleware: [...route.middleware],
449
+ ...(route.domain === undefined ? {} : { domain: route.domain }),
450
+ matchers: { ...route.matchers },
451
+ }
452
+ }
405
453
  }
406
454
 
407
455
  // ─── GroupBuilder ───────────────────────────────────────────
@@ -415,6 +463,16 @@ export class GroupBuilder extends Macroable {
415
463
  this.#routes = routes
416
464
  }
417
465
 
466
+ /**
467
+ * The routes this group wraps (AdonisJS `group.routes`).
468
+ *
469
+ * Read-only view: a group applies to its members, it does not hand out the
470
+ * backing array for someone else to splice.
471
+ */
472
+ get routes(): readonly RouteDefinition[] {
473
+ return [...this.#routes]
474
+ }
475
+
418
476
  /** Set URL prefix for all routes in the group. */
419
477
  prefix(p: string): this {
420
478
  for (const route of this.#routes) {
@@ -705,6 +763,8 @@ function isViewEngine(value: unknown): value is ViewEngine {
705
763
  export class OnRouteBuilder {
706
764
  #router: Router
707
765
  #path: string
766
+ /** The route built by `render`/`redirect`/`redirectToPath`/`setHandler`. */
767
+ #route: RouteBuilder | undefined
708
768
 
709
769
  constructor(router: Router, path: string) {
710
770
  this.#router = router
@@ -721,7 +781,7 @@ export class OnRouteBuilder {
721
781
  * seeds its own engine there.
722
782
  */
723
783
  render(view: string, data?: Record<string, unknown>): RouteBuilder {
724
- return this.#router.get(this.#path, async (ctx) => {
784
+ this.#route = this.#router.get(this.#path, async (ctx) => {
725
785
  const candidate = Reflect.get(Object(ctx), 'view') ?? ctx.store.get('view')
726
786
  if (!isViewEngine(candidate)) {
727
787
  throw new Error(
@@ -732,6 +792,7 @@ export class OnRouteBuilder {
732
792
  const html = await candidate.render(view, data)
733
793
  ctx.response.type('text/html; charset=utf-8').send(html)
734
794
  })
795
+ return this.#route
735
796
  }
736
797
 
737
798
  /**
@@ -740,16 +801,38 @@ export class OnRouteBuilder {
740
801
  * for a fixed URL.
741
802
  */
742
803
  redirect(name: string, params?: Record<string, string>, status = 302): RouteBuilder {
743
- return this.#router.get(this.#path, async (ctx) => {
804
+ this.#route = this.#router.get(this.#path, async (ctx) => {
744
805
  ctx.response.redirect().status(status).toRoute(name, params)
745
806
  })
807
+ return this.#route
746
808
  }
747
809
 
748
810
  /** Redirect to a fixed path/URL (AdonisJS brisk `redirectToPath`). */
749
811
  redirectToPath(target: string, status = 302): RouteBuilder {
750
- return this.#router.get(this.#path, async (ctx) => {
812
+ this.#route = this.#router.get(this.#path, async (ctx) => {
751
813
  ctx.response.redirect().status(status).toPath(target)
752
814
  })
815
+ return this.#route
816
+ }
817
+
818
+ /**
819
+ * Register a handler for this path (AdonisJS brisk `setHandler`).
820
+ *
821
+ * The escape hatch from the canned briskness: `render` and `redirect` cover
822
+ * the two common cases, this covers the third without going back through
823
+ * `router.get()`.
824
+ */
825
+ setHandler(handler: RouteHandlerFunction): RouteBuilder {
826
+ this.#route = this.#router.get(this.#path, handler)
827
+ return this.#route
828
+ }
829
+
830
+ /**
831
+ * The route the last `render`/`redirect`/`setHandler` created, or
832
+ * `undefined` while none has been called (AdonisJS brisk `route`).
833
+ */
834
+ get route(): RouteBuilder | undefined {
835
+ return this.#route
753
836
  }
754
837
  }
755
838
 
@@ -1236,7 +1319,7 @@ export class Router extends Macroable {
1236
1319
  makeSignedUrl(name: string, params?: Record<string, string>, options?: SignedUrlOptions): string {
1237
1320
  if (!this.#signedUrl) {
1238
1321
  throw new Error(
1239
- '[E_NO_APP_KEY] makeSignedUrl() requires an APP_KEY-backed signer. Set APP_KEY (>= 16 chars) so the encryption service is registered.',
1322
+ '[E_MISSING_APP_KEY] makeSignedUrl() requires an APP_KEY-backed signer. Set APP_KEY (>= 16 chars) so the encryption service is registered.',
1240
1323
  )
1241
1324
  }
1242
1325
  const path = this.urlFor(name, params)
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Application } from '../Application.js'
11
+ import { createServiceProxy } from './createServiceProxy.js'
11
12
 
12
13
  let instance: Application | undefined
13
14
 
@@ -25,28 +26,10 @@ export function clearApp(app: Application): void {
25
26
  if (instance === app) instance = undefined
26
27
  }
27
28
 
28
- const app: Application = new Proxy({} as Application, {
29
- get(_target, prop) {
30
- // A module loader inspects what it imports before anyone uses it: it reads
31
- // `then` to decide whether the namespace is thenable, and various symbols
32
- // for interop and formatting. Throwing on those turns a plain
33
- // `import { setX } from '.../services/x'` into a crash at import time, far
34
- // from any real use. They are not members of what this stands in for, so
35
- // answer undefined and let a genuine access be the one that reports.
36
- if (typeof prop === 'symbol' || prop === 'then') {
37
- return undefined
38
- }
39
- if (!instance) {
40
- throw new Error(
41
- 'Application accessed before initialization. ' +
42
- 'Ensure this code runs during or after the boot phase.',
43
- )
44
- }
45
- // Bind methods so private-field writes inside the class resolve
46
- // against the real instance brand instead of the Proxy.
47
- const value = Reflect.get(instance, prop, instance)
48
- return typeof value === 'function' ? value.bind(instance) : value
49
- },
50
- })
29
+ const app: Application = createServiceProxy<Application>(
30
+ () => instance,
31
+ 'Application accessed before initialization. ' +
32
+ 'Ensure this code runs during or after the boot phase.',
33
+ )
51
34
 
52
35
  export default app
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { Console } from '../console/Console.js'
12
+ import { createServiceProxy } from './createServiceProxy.js'
12
13
 
13
14
  let instance: Console | undefined
14
15
 
@@ -26,26 +27,10 @@ export function clearConsole(consoleApp: Console): void {
26
27
  if (instance === consoleApp) instance = undefined
27
28
  }
28
29
 
29
- const consoleApp: Console = new Proxy({} as Console, {
30
- get(_target, prop) {
31
- // A module loader inspects what it imports before anyone uses it: it reads
32
- // `then` to decide whether the namespace is thenable, and various symbols
33
- // for interop and formatting. Throwing on those turns a plain
34
- // `import { setX } from '.../services/x'` into a crash at import time, far
35
- // from any real use. They are not members of what this stands in for, so
36
- // answer undefined and let a genuine access be the one that reports.
37
- if (typeof prop === 'symbol' || prop === 'then') {
38
- return undefined
39
- }
40
- if (!instance) {
41
- throw new Error(
42
- 'Console accessed before initialization. ' +
43
- 'Ensure this code runs during or after the boot phase.',
44
- )
45
- }
46
- const value = Reflect.get(instance, prop, instance)
47
- return typeof value === 'function' ? value.bind(instance) : value
48
- },
49
- })
30
+ const consoleApp: Console = createServiceProxy<Console>(
31
+ () => instance,
32
+ 'Console accessed before initialization. ' +
33
+ 'Ensure this code runs during or after the boot phase.',
34
+ )
50
35
 
51
36
  export default consoleApp
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The one implementation behind every `@c9up/ream/services/*` accessor.
3
+ *
4
+ * Each accessor stands in for a singleton that only exists once the Ignitor has
5
+ * built it, so the module-level export is a proxy that forwards to whatever the
6
+ * boot phase installed. Written out per service, the proxy was copied five
7
+ * times and the `symbol`/`then` guard below reached only three of them — a fix
8
+ * that has to be applied by hand five times is a fix that will be missed.
9
+ *
10
+ * @see services/app.ts for the accessor shape this backs.
11
+ */
12
+
13
+ /**
14
+ * Build the proxy an accessor module default-exports.
15
+ *
16
+ * @param read Returns the live singleton, or `undefined` before boot.
17
+ * @param notReady Message thrown when something reads the accessor too early.
18
+ */
19
+ export function createServiceProxy<T extends object>(
20
+ read: () => T | undefined,
21
+ notReady: string,
22
+ ): T {
23
+ // The target is never read through — every trap forwards to `read()`.
24
+ const target: T = Object.create(null)
25
+ return new Proxy(target, {
26
+ get(_target, prop) {
27
+ // A module loader inspects what it imports before anyone uses it: it
28
+ // reads `then` to decide whether the namespace is thenable, and various
29
+ // symbols for interop and formatting. Throwing on those turns a plain
30
+ // `import { setX } from '.../services/x'` into a crash at import time,
31
+ // far from any real use. They are not members of what this stands in
32
+ // for, so answer undefined and let a genuine access be the one that
33
+ // reports.
34
+ if (typeof prop === 'symbol' || prop === 'then') {
35
+ return undefined
36
+ }
37
+ const instance = read()
38
+ if (!instance) {
39
+ throw new Error(notReady)
40
+ }
41
+ // Bind methods so private-field writes inside the class resolve against
42
+ // the real instance brand instead of the Proxy.
43
+ const value = Reflect.get(instance, prop, instance)
44
+ return typeof value === 'function' ? value.bind(instance) : value
45
+ },
46
+ })
47
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { Router } from '../router/Router.js'
12
+ import { createServiceProxy } from './createServiceProxy.js'
12
13
 
13
14
  let instance: Router | undefined
14
15
 
@@ -34,19 +35,10 @@ export function getRouter(): Router | undefined {
34
35
  * Router proxy — defers all property access to the underlying instance.
35
36
  * Throws if accessed before Ignitor initializes it.
36
37
  */
37
- const router: Router = new Proxy({} as Router, {
38
- get(_target, prop) {
39
- if (!instance) {
40
- throw new Error(
41
- 'Router accessed before initialization. ' +
42
- 'Ensure your route files are loaded as preloads in reamrc.ts, not at import time.',
43
- )
44
- }
45
- // Bind methods so private-field writes inside the class resolve
46
- // against the real instance brand instead of the Proxy.
47
- const value = Reflect.get(instance, prop, instance)
48
- return typeof value === 'function' ? value.bind(instance) : value
49
- },
50
- })
38
+ const router: Router = createServiceProxy<Router>(
39
+ () => instance,
40
+ 'Router accessed before initialization. ' +
41
+ 'Ensure your route files are loaded as preloads in reamrc.ts, not at import time.',
42
+ )
51
43
 
52
44
  export default router
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Server } from '../server/Server.js'
11
+ import { createServiceProxy } from './createServiceProxy.js'
11
12
 
12
13
  let instance: Server | undefined
13
14
 
@@ -29,29 +30,10 @@ export function getServer(): Server | undefined {
29
30
  return instance
30
31
  }
31
32
 
32
- const server: Server = new Proxy({} as Server, {
33
- get(_target, prop) {
34
- // A module loader inspects what it imports before anyone uses it: it reads
35
- // `then` to decide whether the namespace is thenable, and various symbols
36
- // for interop and formatting. Throwing on those turns a plain
37
- // `import { setX } from '.../services/x'` into a crash at import time, far
38
- // from any real use. They are not members of what this stands in for, so
39
- // answer undefined and let a genuine access be the one that reports.
40
- if (typeof prop === 'symbol' || prop === 'then') {
41
- return undefined
42
- }
43
- if (!instance) {
44
- throw new Error(
45
- 'Server accessed before initialization. ' +
46
- 'Ensure your kernel files are loaded as preloads in reamrc.ts.',
47
- )
48
- }
49
- // Bind methods to the real instance so private-field access inside
50
- // them resolves against the underlying class brand — passing the
51
- // Proxy as `this` breaks `#field` writes (Proxy isn't branded).
52
- const value = Reflect.get(instance, prop, instance)
53
- return typeof value === 'function' ? value.bind(instance) : value
54
- },
55
- })
33
+ const server: Server = createServiceProxy<Server>(
34
+ () => instance,
35
+ 'Server accessed before initialization. ' +
36
+ 'Ensure your kernel files are loaded as preloads in reamrc.ts.',
37
+ )
56
38
 
57
39
  export default server
@@ -49,4 +49,24 @@ export class ReadOnlyValuesStore {
49
49
  has(path: ValuePath): boolean {
50
50
  return this.get(path) !== undefined
51
51
  }
52
+
53
+ /**
54
+ * Every value, as a plain object (AdonisJS `toObject`).
55
+ *
56
+ * Same content as {@link all}; the two names exist because AdonisJS ships
57
+ * both and a migrated template may reach for either.
58
+ */
59
+ toObject(): Record<string, unknown> {
60
+ return this.all()
61
+ }
62
+
63
+ /** Every value — what `JSON.stringify(flashMessages)` emits. */
64
+ toJSON(): Record<string, unknown> {
65
+ return this.all()
66
+ }
67
+
68
+ /** JSON text, so `{{ flashMessages }}` in a template renders the contents. */
69
+ toString(): string {
70
+ return JSON.stringify(this.#values)
71
+ }
52
72
  }
@@ -11,6 +11,8 @@
11
11
  */
12
12
 
13
13
  import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
14
+ import { E_SESSION_NOT_READY, E_SESSION_TAGGING_NOT_SUPPORTED } from './errors.js'
15
+ import { ReadOnlyValuesStore } from './ReadOnlyValuesStore.js'
14
16
 
15
17
  export interface SessionDriver {
16
18
  /**
@@ -435,9 +437,16 @@ export class Session {
435
437
  this.forget(INTENDED_URL_KEY)
436
438
  }
437
439
 
438
- /** Get a flashed value from the previous request. */
439
- flashMessages(): Record<string, unknown> {
440
- return { ...this.#previousFlash }
440
+ /**
441
+ * What the PREVIOUS request flashed for this one (AdonisJS `flashMessages`).
442
+ *
443
+ * A property holding a store, not a method returning a record: that is the
444
+ * AdonisJS shape, and it is what lets a controller or template read a nested
445
+ * failure with `session.flashMessages.get('errors.email')`. `all()` gives
446
+ * back the plain object.
447
+ */
448
+ get flashMessages(): ReadOnlyValuesStore {
449
+ return new ReadOnlyValuesStore(this.#previousFlash)
441
450
  }
442
451
 
443
452
  /** Get a specific flash message from the previous request. */
@@ -519,8 +528,8 @@ export class Session {
519
528
  * live. `flashMessages` is the other half — what the PREVIOUS request left
520
529
  * for this one.
521
530
  */
522
- get responseFlashMessages(): Record<string, unknown> {
523
- return { ...this.#flashData }
531
+ get responseFlashMessages(): ReadOnlyValuesStore {
532
+ return new ReadOnlyValuesStore(this.#flashData)
524
533
  }
525
534
 
526
535
  /**
@@ -625,14 +634,10 @@ export class Session {
625
634
  */
626
635
  #taggingDriver(method: string): SessionDriverWithTagging {
627
636
  if (this.#driver === undefined) {
628
- throw new Error(
629
- `session.${method}() needs a session store — this session was built without one.`,
630
- )
637
+ throw new E_SESSION_NOT_READY(method)
631
638
  }
632
639
  if (!supportsTagging(this.#driver)) {
633
- throw new Error(
634
- `session.${method}() is not supported by the configured session store. Use the memory, redis, or database store.`,
635
- )
640
+ throw new E_SESSION_TAGGING_NOT_SUPPORTED(method)
636
641
  }
637
642
  return this.#driver
638
643
  }
@@ -232,7 +232,8 @@ function shareSessionWithView(ctx: HttpContext, session: Session): void {
232
232
  const view = Reflect.get(Object(ctx), 'view')
233
233
  const share = Reflect.get(Object(view), 'share')
234
234
  if (typeof share !== 'function') return
235
- const flashMessages = new ReadOnlyValuesStore(session.flashMessages())
235
+ // The session hands back a store already — no need to rebuild one here.
236
+ const flashMessages = session.flashMessages
236
237
  share.call(view, {
237
238
  session: new ReadOnlyValuesStore(session.all()),
238
239
  flashMessages,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Session exceptions, under the codes AdonisJS raises.
3
+ *
4
+ * The conditions were already guarded — they just threw bare `Error`s, so an
5
+ * app could only match on the message text.
6
+ *
7
+ * `E_SESSION_NOT_MUTABLE` has no counterpart here on purpose: it is what
8
+ * AdonisJS raises for a session initiated in read-only mode, and ream has no
9
+ * read-only mode. Declaring the class with nothing able to throw it would be
10
+ * worse than its absence — an app would guard a branch that never runs.
11
+ */
12
+
13
+ import { Exception } from '../http/Exception.js'
14
+
15
+ /** The session was used before a store was attached to it. */
16
+ export class E_SESSION_NOT_READY extends Exception {
17
+ static override status = 500
18
+ static override code = 'E_SESSION_NOT_READY'
19
+
20
+ constructor(method: string) {
21
+ super(`session.${method}() needs a session store — this session was built without one.`, {
22
+ status: 500,
23
+ code: 'E_SESSION_NOT_READY',
24
+ })
25
+ }
26
+ }
27
+
28
+ /** The configured store cannot tag sessions (cookie and file stores cannot). */
29
+ export class E_SESSION_TAGGING_NOT_SUPPORTED extends Exception {
30
+ static override status = 500
31
+ static override code = 'E_SESSION_TAGGING_NOT_SUPPORTED'
32
+
33
+ constructor(method: string) {
34
+ super(
35
+ `session.${method}() is not supported by the configured session store. Use the memory, redis, or database store.`,
36
+ { status: 500, code: 'E_SESSION_TAGGING_NOT_SUPPORTED' },
37
+ )
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Guard name → what `auth.use(name)` hands back.
3
+ *
4
+ * Empty here, and deliberately: ream owns no auth package, so it cannot know
5
+ * the guards an app configured. The auth package fills it in, exactly as
6
+ * AdonisJS does with `Authenticators`:
7
+ *
8
+ * ```ts
9
+ * declare module '@c9up/ream/types' {
10
+ * interface Authenticators {
11
+ * session: SessionGuard
12
+ * jwt: JwtGuard
13
+ * }
14
+ * }
15
+ * ```
16
+ *
17
+ * Until something augments it, `keyof Authenticators` is `never` and `use()`
18
+ * falls through to its `unknown` signature — the behaviour ream had before,
19
+ * kept as the floor rather than as the ceiling.
20
+ */
21
+ export type Authenticators = {}
22
+
23
+ /** A guard name this application knows about. */
24
+ export type AuthenticatorName = keyof Authenticators