@c9up/ream 0.1.24 → 0.1.26

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 (172) hide show
  1. package/dist/ConfigLoader.d.ts +46 -5
  2. package/dist/ConfigLoader.d.ts.map +1 -1
  3. package/dist/ConfigLoader.js +60 -9
  4. package/dist/ConfigLoader.js.map +1 -1
  5. package/dist/HttpKernel.d.ts.map +1 -1
  6. package/dist/HttpKernel.js +46 -6
  7. package/dist/HttpKernel.js.map +1 -1
  8. package/dist/Ignitor.d.ts.map +1 -1
  9. package/dist/Ignitor.js +21 -3
  10. package/dist/Ignitor.js.map +1 -1
  11. package/dist/container/Container.d.ts +23 -3
  12. package/dist/container/Container.d.ts.map +1 -1
  13. package/dist/container/Container.js +139 -60
  14. package/dist/container/Container.js.map +1 -1
  15. package/dist/env/Env.d.ts +29 -4
  16. package/dist/env/Env.d.ts.map +1 -1
  17. package/dist/env/Env.js +27 -4
  18. package/dist/env/Env.js.map +1 -1
  19. package/dist/env/Secret.d.ts +11 -0
  20. package/dist/env/Secret.d.ts.map +1 -0
  21. package/dist/env/Secret.js +36 -0
  22. package/dist/env/Secret.js.map +1 -0
  23. package/dist/env/interpolate.d.ts +26 -0
  24. package/dist/env/interpolate.d.ts.map +1 -0
  25. package/dist/env/interpolate.js +48 -0
  26. package/dist/env/interpolate.js.map +1 -0
  27. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  28. package/dist/env/loadEnvFiles.js +14 -4
  29. package/dist/env/loadEnvFiles.js.map +1 -1
  30. package/dist/env/schema.d.ts +23 -6
  31. package/dist/env/schema.d.ts.map +1 -1
  32. package/dist/env/schema.js +67 -33
  33. package/dist/env/schema.js.map +1 -1
  34. package/dist/events/Emitter.d.ts +1 -1
  35. package/dist/events/Emitter.d.ts.map +1 -1
  36. package/dist/events/Emitter.js +1 -1
  37. package/dist/events/Emitter.js.map +1 -1
  38. package/dist/events/EventsProvider.d.ts +2 -2
  39. package/dist/events/EventsProvider.d.ts.map +1 -1
  40. package/dist/events/EventsProvider.js +3 -3
  41. package/dist/events/EventsProvider.js.map +1 -1
  42. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  43. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  44. package/dist/graphql/GraphQLEngine.js +2 -2
  45. package/dist/graphql/GraphQLEngine.js.map +1 -1
  46. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  47. package/dist/graphql/GraphQLProvider.js +7 -3
  48. package/dist/graphql/GraphQLProvider.js.map +1 -1
  49. package/dist/http/Exception.d.ts +63 -1
  50. package/dist/http/Exception.d.ts.map +1 -1
  51. package/dist/http/Exception.js +185 -48
  52. package/dist/http/Exception.js.map +1 -1
  53. package/dist/http/HttpContext.d.ts +56 -9
  54. package/dist/http/HttpContext.d.ts.map +1 -1
  55. package/dist/http/HttpContext.js +56 -15
  56. package/dist/http/HttpContext.js.map +1 -1
  57. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  58. package/dist/http/RedirectBuilder.js +4 -1
  59. package/dist/http/RedirectBuilder.js.map +1 -1
  60. package/dist/http/Request.d.ts +141 -10
  61. package/dist/http/Request.d.ts.map +1 -1
  62. package/dist/http/Request.js +244 -34
  63. package/dist/http/Request.js.map +1 -1
  64. package/dist/http/Response.d.ts +176 -9
  65. package/dist/http/Response.d.ts.map +1 -1
  66. package/dist/http/Response.js +431 -19
  67. package/dist/http/Response.js.map +1 -1
  68. package/dist/index.d.ts +5 -4
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/middleware/Pipeline.js +1 -1
  73. package/dist/middleware/Pipeline.js.map +1 -1
  74. package/dist/openapi/OpenApiProvider.js +4 -4
  75. package/dist/openapi/OpenApiProvider.js.map +1 -1
  76. package/dist/router/Router.d.ts +223 -31
  77. package/dist/router/Router.d.ts.map +1 -1
  78. package/dist/router/Router.js +520 -102
  79. package/dist/router/Router.js.map +1 -1
  80. package/dist/rpc/RpcProvider.d.ts +1 -0
  81. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  82. package/dist/rpc/RpcProvider.js +7 -3
  83. package/dist/rpc/RpcProvider.js.map +1 -1
  84. package/dist/rpc/RpcRouter.d.ts +2 -2
  85. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  86. package/dist/rpc/RpcRouter.js +5 -4
  87. package/dist/rpc/RpcRouter.js.map +1 -1
  88. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  89. package/dist/scheduler/ScheduleProvider.js +7 -15
  90. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  91. package/dist/server/Server.d.ts +17 -4
  92. package/dist/server/Server.d.ts.map +1 -1
  93. package/dist/server/Server.js +41 -7
  94. package/dist/server/Server.js.map +1 -1
  95. package/dist/session/SessionMiddleware.js +3 -3
  96. package/dist/session/SessionMiddleware.js.map +1 -1
  97. package/dist/storage/StaticProvider.js +1 -1
  98. package/dist/storage/StaticProvider.js.map +1 -1
  99. package/dist/testing/RequestBuilder.d.ts +60 -0
  100. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  101. package/dist/testing/RequestBuilder.js +180 -1
  102. package/dist/testing/RequestBuilder.js.map +1 -1
  103. package/dist/testing/TestClient.d.ts +22 -1
  104. package/dist/testing/TestClient.d.ts.map +1 -1
  105. package/dist/testing/TestClient.js +40 -0
  106. package/dist/testing/TestClient.js.map +1 -1
  107. package/dist/utils/Macroable.d.ts +30 -0
  108. package/dist/utils/Macroable.d.ts.map +1 -0
  109. package/dist/utils/Macroable.js +43 -0
  110. package/dist/utils/Macroable.js.map +1 -0
  111. package/dist/utils/inflect.d.ts +17 -0
  112. package/dist/utils/inflect.d.ts.map +1 -0
  113. package/dist/utils/inflect.js +32 -0
  114. package/dist/utils/inflect.js.map +1 -0
  115. package/dist/utils/objectPath.d.ts +57 -0
  116. package/dist/utils/objectPath.d.ts.map +1 -0
  117. package/dist/utils/objectPath.js +141 -0
  118. package/dist/utils/objectPath.js.map +1 -0
  119. package/events.darwin-arm64.node +0 -0
  120. package/events.darwin-x64.node +0 -0
  121. package/events.linux-arm64-gnu.node +0 -0
  122. package/events.linux-x64-gnu.node +0 -0
  123. package/events.win32-x64-msvc.node +0 -0
  124. package/index.darwin-arm64.node +0 -0
  125. package/index.darwin-x64.node +0 -0
  126. package/index.linux-arm64-gnu.node +0 -0
  127. package/index.linux-x64-gnu.node +0 -0
  128. package/index.win32-x64-msvc.node +0 -0
  129. package/package.json +6 -1
  130. package/scheduler.darwin-arm64.node +0 -0
  131. package/scheduler.darwin-x64.node +0 -0
  132. package/scheduler.linux-arm64-gnu.node +0 -0
  133. package/scheduler.linux-x64-gnu.node +0 -0
  134. package/scheduler.win32-x64-msvc.node +0 -0
  135. package/src/ConfigLoader.ts +72 -9
  136. package/src/HttpKernel.ts +53 -15
  137. package/src/Ignitor.ts +24 -3
  138. package/src/container/Container.ts +145 -61
  139. package/src/env/Env.ts +49 -5
  140. package/src/env/Secret.ts +41 -0
  141. package/src/env/interpolate.ts +54 -0
  142. package/src/env/loadEnvFiles.ts +12 -4
  143. package/src/env/schema.ts +109 -35
  144. package/src/events/Emitter.ts +2 -2
  145. package/src/events/EventsProvider.ts +5 -5
  146. package/src/graphql/GraphQLEngine.ts +4 -4
  147. package/src/graphql/GraphQLProvider.ts +7 -3
  148. package/src/http/Exception.ts +214 -37
  149. package/src/http/HttpContext.ts +93 -20
  150. package/src/http/RedirectBuilder.ts +4 -1
  151. package/src/http/Request.ts +264 -34
  152. package/src/http/Response.ts +504 -30
  153. package/src/index.ts +11 -2
  154. package/src/middleware/Pipeline.ts +1 -1
  155. package/src/openapi/OpenApiProvider.ts +4 -4
  156. package/src/router/Router.ts +631 -113
  157. package/src/rpc/RpcProvider.ts +6 -2
  158. package/src/rpc/RpcRouter.ts +7 -6
  159. package/src/scheduler/ScheduleProvider.ts +7 -15
  160. package/src/server/Server.ts +49 -10
  161. package/src/session/SessionMiddleware.ts +4 -4
  162. package/src/storage/StaticProvider.ts +1 -1
  163. package/src/testing/RequestBuilder.ts +218 -1
  164. package/src/testing/TestClient.ts +57 -1
  165. package/src/utils/Macroable.ts +54 -0
  166. package/src/utils/inflect.ts +29 -0
  167. package/src/utils/objectPath.ts +142 -0
  168. package/dist/decorators/Lazy.d.ts +0 -19
  169. package/dist/decorators/Lazy.d.ts.map +0 -1
  170. package/dist/decorators/Lazy.js +0 -53
  171. package/dist/decorators/Lazy.js.map +0 -1
  172. package/src/decorators/Lazy.ts +0 -57
package/src/env/schema.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  * value as `undefined`.
14
14
  */
15
15
 
16
+ import { Secret } from './Secret.js'
17
+
16
18
  /** A single failed-variable validation — collected by `Env.create`. */
17
19
  export class EnvVarError extends Error {
18
20
  constructor(message: string) {
@@ -26,14 +28,27 @@ export interface SchemaNode<T> {
26
28
  validate(name: string, value: string | undefined): T
27
29
  }
28
30
 
29
- /** A required node that can be turned optional — `.optional()` widens `T` to `T | undefined`. */
31
+ /** Condition deciding whether a variable is optional (AdonisJS `optionalWhen`). */
32
+ export type OptionalCondition = boolean | ((name: string, value: string | undefined) => boolean)
33
+
34
+ /**
35
+ * A required node that can be turned optional — `.optional()` widens `T` to
36
+ * `T | undefined`; `.optionalWhen(cond)` does so only when `cond` holds.
37
+ */
30
38
  export interface RequiredNode<T> extends SchemaNode<T> {
31
39
  optional(): SchemaNode<T | undefined>
40
+ optionalWhen(condition: OptionalCondition): SchemaNode<T | undefined>
32
41
  }
33
42
 
34
- export interface StringOptions {
43
+ /** Options common to every schema function (AdonisJS `SchemaFnOptions`). */
44
+ export interface SchemaFnOptions {
45
+ /** Custom error message, replacing the default when validation fails. */
46
+ message?: string
47
+ }
48
+
49
+ export interface StringOptions extends SchemaFnOptions {
35
50
  /** Validate the string against a known format. */
36
- format?: 'host' | 'url' | 'email'
51
+ format?: 'host' | 'url' | 'email' | 'uuid'
37
52
  /** For `format: 'url'` — require a TLD (default true). */
38
53
  tld?: boolean
39
54
  /** For `format: 'url'` — require a protocol (default true). */
@@ -45,24 +60,30 @@ const FALSE_VALUES = new Set(['false', '0'])
45
60
 
46
61
  /**
47
62
  * Build a node from a `parse` that only ever sees a present, non-empty string.
48
- * Required `validate` throws on absent/empty; `.optional()` returns `undefined`.
63
+ * Required `validate` throws on absent/empty (using `message` when given);
64
+ * `.optional()` returns `undefined`; `.optionalWhen(cond)` is optional only
65
+ * when the condition holds.
49
66
  */
50
- function makeNode<T>(parse: (name: string, raw: string) => T): RequiredNode<T> {
67
+ function makeNode<T>(parse: (name: string, raw: string) => T, message?: string): RequiredNode<T> {
68
+ const required = (name: string, value: string | undefined): T => {
69
+ if (value === undefined || value === '') {
70
+ throw new EnvVarError(message ?? `Missing required environment variable "${name}"`)
71
+ }
72
+ return parse(name, value)
73
+ }
74
+ const optional = (name: string, value: string | undefined): T | undefined => {
75
+ if (value === undefined || value === '') return undefined
76
+ return parse(name, value)
77
+ }
51
78
  return {
52
- validate(name, value) {
53
- if (value === undefined || value === '') {
54
- throw new EnvVarError(`Missing required environment variable "${name}"`)
55
- }
56
- return parse(name, value)
57
- },
58
- optional() {
59
- return {
60
- validate(name, value) {
61
- if (value === undefined || value === '') return undefined
62
- return parse(name, value)
63
- },
64
- }
65
- },
79
+ validate: required,
80
+ optional: () => ({ validate: optional }),
81
+ optionalWhen: (condition) => ({
82
+ validate(name, value) {
83
+ const skip = typeof condition === 'function' ? condition(name, value) : condition
84
+ return skip ? optional(name, value) : required(name, value)
85
+ },
86
+ }),
66
87
  }
67
88
  }
68
89
 
@@ -93,6 +114,10 @@ function isEmail(value: string): boolean {
93
114
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
94
115
  }
95
116
 
117
+ function isUuid(value: string): boolean {
118
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
119
+ }
120
+
96
121
  function checkFormat(name: string, value: string, options?: StringOptions): void {
97
122
  if (!options?.format) return
98
123
  const ok =
@@ -100,10 +125,13 @@ function checkFormat(name: string, value: string, options?: StringOptions): void
100
125
  ? isHost(value)
101
126
  : options.format === 'url'
102
127
  ? isUrl(value, options)
103
- : isEmail(value)
128
+ : options.format === 'uuid'
129
+ ? isUuid(value)
130
+ : isEmail(value)
104
131
  if (!ok) {
105
132
  throw new EnvVarError(
106
- `Environment variable "${name}" must be a valid ${options.format}, got "${value}"`,
133
+ options.message ??
134
+ `Environment variable "${name}" must be a valid ${options.format}, got "${value}"`,
107
135
  )
108
136
  }
109
137
  }
@@ -115,49 +143,95 @@ function stringParse(options?: StringOptions): (name: string, raw: string) => st
115
143
  }
116
144
  }
117
145
 
118
- /** `string(opts?)` is callable AND exposes `.optional(opts?)` — both Adonis forms. */
146
+ /**
147
+ * `string(opts?)` is callable AND exposes `.optional(opts?)` / `.optionalWhen()`
148
+ * shortcuts — all AdonisJS forms.
149
+ */
119
150
  const string: ((options?: StringOptions) => RequiredNode<string>) & {
120
151
  optional(options?: StringOptions): SchemaNode<string | undefined>
121
- } = Object.assign((options?: StringOptions) => makeNode(stringParse(options)), {
122
- optional: (options?: StringOptions) => makeNode(stringParse(options)).optional(),
152
+ optionalWhen(
153
+ condition: OptionalCondition,
154
+ options?: StringOptions,
155
+ ): SchemaNode<string | undefined>
156
+ } = Object.assign((options?: StringOptions) => makeNode(stringParse(options), options?.message), {
157
+ optional: (options?: StringOptions) =>
158
+ makeNode(stringParse(options), options?.message).optional(),
159
+ optionalWhen: (condition: OptionalCondition, options?: StringOptions) =>
160
+ makeNode(stringParse(options), options?.message).optionalWhen(condition),
123
161
  })
124
162
 
125
- function number(): RequiredNode<number> {
126
- return makeNode((name, raw) => {
163
+ function numberParse(options?: SchemaFnOptions): (name: string, raw: string) => number {
164
+ return (name, raw) => {
127
165
  const parsed = Number(raw)
128
166
  if (Number.isNaN(parsed)) {
129
- throw new EnvVarError(`Environment variable "${name}" must be a number, got "${raw}"`)
167
+ throw new EnvVarError(
168
+ options?.message ?? `Environment variable "${name}" must be a number, got "${raw}"`,
169
+ )
130
170
  }
131
171
  return parsed
132
- })
172
+ }
133
173
  }
134
174
 
135
- function boolean(): RequiredNode<boolean> {
136
- return makeNode((name, raw) => {
175
+ function number(options?: SchemaFnOptions): RequiredNode<number> {
176
+ return makeNode(numberParse(options), options?.message)
177
+ }
178
+
179
+ function booleanParse(options?: SchemaFnOptions): (name: string, raw: string) => boolean {
180
+ return (name, raw) => {
137
181
  if (TRUE_VALUES.has(raw)) return true
138
182
  if (FALSE_VALUES.has(raw)) return false
139
183
  throw new EnvVarError(
140
- `Environment variable "${name}" must be a boolean (true/false/1/0), got "${raw}"`,
184
+ options?.message ??
185
+ `Environment variable "${name}" must be a boolean (true/false/1/0), got "${raw}"`,
141
186
  )
142
- })
187
+ }
188
+ }
189
+
190
+ function boolean(options?: SchemaFnOptions): RequiredNode<boolean> {
191
+ return makeNode(booleanParse(options), options?.message)
143
192
  }
144
193
 
145
- function enumNode<V extends readonly string[]>(values: V): RequiredNode<V[number]> {
194
+ function enumNode<V extends readonly string[]>(
195
+ values: V,
196
+ options?: SchemaFnOptions,
197
+ ): RequiredNode<V[number]> {
146
198
  return makeNode((name, raw) => {
147
199
  const match = values.find((candidate) => candidate === raw)
148
200
  if (match === undefined) {
149
201
  throw new EnvVarError(
150
- `Environment variable "${name}" must be one of [${values.join(', ')}], got "${raw}"`,
202
+ options?.message ??
203
+ `Environment variable "${name}" must be one of [${values.join(', ')}], got "${raw}"`,
151
204
  )
152
205
  }
153
206
  return match
154
- })
207
+ }, options?.message)
155
208
  }
156
209
 
210
+ /**
211
+ * `secret(opts?)` wraps the value in a {@link Secret} so it never leaks through
212
+ * logs/JSON (AdonisJS `Env.schema.secret`). Read it with `.release()`.
213
+ */
214
+ const secret: ((options?: SchemaFnOptions) => RequiredNode<Secret<string>>) & {
215
+ optional(options?: SchemaFnOptions): SchemaNode<Secret<string> | undefined>
216
+ optionalWhen(
217
+ condition: OptionalCondition,
218
+ options?: SchemaFnOptions,
219
+ ): SchemaNode<Secret<string> | undefined>
220
+ } = Object.assign(
221
+ (options?: SchemaFnOptions) => makeNode((_name, raw) => new Secret(raw), options?.message),
222
+ {
223
+ optional: (options?: SchemaFnOptions) =>
224
+ makeNode((_name, raw) => new Secret(raw), options?.message).optional(),
225
+ optionalWhen: (condition: OptionalCondition, options?: SchemaFnOptions) =>
226
+ makeNode((_name, raw) => new Secret(raw), options?.message).optionalWhen(condition),
227
+ },
228
+ )
229
+
157
230
  /** The `Env.schema` surface. */
158
231
  export const schema = {
159
232
  string,
160
233
  number,
161
234
  boolean,
162
235
  enum: enumNode,
236
+ secret,
163
237
  }
@@ -35,7 +35,7 @@ type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
35
35
 
36
36
  /** Resolver for instantiating listener classes with DI. */
37
37
  export interface ContainerResolver {
38
- make<T>(target: new (...args: never[]) => T): T
38
+ make<T>(target: new (...args: never[]) => T): Promise<T>
39
39
  }
40
40
 
41
41
  export class Emitter {
@@ -135,7 +135,7 @@ export class Emitter {
135
135
  if (isListenerClass(listener)) {
136
136
  // Listener class — resolve via container for @inject() support
137
137
  const instance = this.resolver
138
- ? this.resolver.make(listener as ListenerConstructor<T>)
138
+ ? await this.resolver.make(listener as ListenerConstructor<T>)
139
139
  : new (listener as ListenerConstructor<T>)()
140
140
  await instance.handle(event)
141
141
  } else {
@@ -15,8 +15,8 @@ import { setEmitter } from './services/main.js'
15
15
  */
16
16
  interface EventsContainer {
17
17
  singleton(token: unknown, factory: () => unknown): void
18
- resolve<T = unknown>(token: unknown): T
19
- make?<T>(target: new (...args: never[]) => T): T
18
+ resolve<T = unknown>(token: unknown): Promise<T>
19
+ make?<T>(target: new (...args: never[]) => T): Promise<T>
20
20
  }
21
21
  export interface EventsAppContext {
22
22
  container: EventsContainer
@@ -31,8 +31,8 @@ export default class EventsProvider {
31
31
  this.app.container.singleton(EventBus, () => new EventBus())
32
32
  this.app.container.singleton('bus', () => this.app.container.resolve<EventBus>(EventBus))
33
33
 
34
- this.app.container.singleton(Emitter, () => {
35
- const bus = this.app.container.resolve<EventBus>(EventBus)
34
+ this.app.container.singleton(Emitter, async () => {
35
+ const bus = await this.app.container.resolve<EventBus>(EventBus)
36
36
  // Forward `make` only when the host actually exposes it.
37
37
  // Emitter falls back to direct `new Listener()` instantiation
38
38
  // otherwise, which keeps zero-arg listener classes working
@@ -49,7 +49,7 @@ export default class EventsProvider {
49
49
  }
50
50
 
51
51
  async boot() {
52
- this.#emitter = this.app.container.resolve<Emitter>(Emitter)
52
+ this.#emitter = await this.app.container.resolve<Emitter>(Emitter)
53
53
  BaseEvent.useEmitter(this.#emitter)
54
54
  setEmitter(this.#emitter)
55
55
  }
@@ -203,7 +203,7 @@ export interface SelectionField {
203
203
  export class GraphQLEngine {
204
204
  #schemaSource: string
205
205
  #resolvers: Map<string, ResolverEntry> = new Map()
206
- #container?: { make<T>(target: new (...args: unknown[]) => T): T }
206
+ #container?: { make<T>(target: new (...args: unknown[]) => T): Promise<T> }
207
207
  readonly path: string
208
208
  #playground: boolean
209
209
  #maxQueryBytes: number
@@ -238,7 +238,7 @@ export class GraphQLEngine {
238
238
  }
239
239
 
240
240
  /** Set IoC container for resolver instantiation. */
241
- useContainer(container: { make<T>(target: new (...args: unknown[]) => T): T }): void {
241
+ useContainer(container: { make<T>(target: new (...args: unknown[]) => T): Promise<T> }): void {
242
242
  this.#container = container
243
243
  }
244
244
 
@@ -369,7 +369,7 @@ export class GraphQLEngine {
369
369
  try {
370
370
  // Resolve handler via IoC or direct instantiation.
371
371
  const instance = this.#container
372
- ? this.#container.make(entry.handlerClass)
372
+ ? await this.#container.make(entry.handlerClass)
373
373
  : new entry.handlerClass()
374
374
 
375
375
  const handler = instance[entry.methodName]
@@ -436,7 +436,7 @@ export class GraphQLEngine {
436
436
  const permissions = options.permissions ?? []
437
437
 
438
438
  const needsAuth = guards.length > 0 || roles.length > 0 || permissions.length > 0
439
- if (needsAuth && !ctx.auth?.authenticated) {
439
+ if (needsAuth && !ctx.auth?.isAuthenticated) {
440
440
  return 'Unauthorized'
441
441
  }
442
442
 
@@ -28,6 +28,7 @@ export interface GraphQLProviderOptions {
28
28
 
29
29
  export class GraphQLProvider extends Provider {
30
30
  #engine?: GraphQLEngine
31
+ #registered = false
31
32
 
32
33
  constructor(app: AppContext, options: GraphQLProviderOptions = {}) {
33
34
  super(app)
@@ -45,9 +46,11 @@ export class GraphQLProvider extends Provider {
45
46
  if (!engine) return // GraphQL not configured — opt-out.
46
47
  const container = this.app.container
47
48
  if (container.has('graphql')) {
48
- if (container.resolve('graphql') === engine) {
49
+ // Idempotent re-register: tracked with a flag rather than
50
+ // `container.resolve('graphql')` because resolution is now async.
51
+ if (this.#registered) {
49
52
  this.#engine = engine
50
- return // re-register is idempotent
53
+ return
51
54
  }
52
55
  throw new ReamError(
53
56
  'GRAPHQL_PROVIDER_ALREADY_REGISTERED',
@@ -60,12 +63,13 @@ export class GraphQLProvider extends Provider {
60
63
  engine.useContainer(container)
61
64
  this.#engine = engine
62
65
  container.singleton('graphql', () => engine)
66
+ this.#registered = true
63
67
  }
64
68
 
65
69
  override async boot(): Promise<void> {
66
70
  const engine = this.#engine
67
71
  if (!engine) return
68
- const router = this.app.container.make<Router>('router')
72
+ const router = await this.app.container.make<Router>('router')
69
73
  // GraphQLEngine.handle() serves the playground on GET and executes queries
70
74
  // on POST, so mount the same path for both verbs.
71
75
  router.get(engine.path, (ctx) => engine.handle(ctx))