@zmdb/web 1.0.0-beta.1

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 (179) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +49 -0
  3. package/dist/app/bridge.d.ts +12 -0
  4. package/dist/app/bridge.d.ts.map +1 -0
  5. package/dist/app/bridge.js +16 -0
  6. package/dist/app/bridge.js.map +1 -0
  7. package/dist/app/index.d.ts +23 -0
  8. package/dist/app/index.d.ts.map +1 -0
  9. package/dist/app/index.js +47 -0
  10. package/dist/app/index.js.map +1 -0
  11. package/dist/bench/index.d.ts +68 -0
  12. package/dist/bench/index.d.ts.map +1 -0
  13. package/dist/bench/index.js +250 -0
  14. package/dist/bench/index.js.map +1 -0
  15. package/dist/compression/index.d.ts +13 -0
  16. package/dist/compression/index.d.ts.map +1 -0
  17. package/dist/compression/index.js +211 -0
  18. package/dist/compression/index.js.map +1 -0
  19. package/dist/context/index.d.ts +74 -0
  20. package/dist/context/index.d.ts.map +1 -0
  21. package/dist/context/index.js +118 -0
  22. package/dist/context/index.js.map +1 -0
  23. package/dist/contract/__fixtures__/http-client.generated.d.ts +70 -0
  24. package/dist/contract/__fixtures__/http-client.generated.d.ts.map +1 -0
  25. package/dist/contract/__fixtures__/http-client.generated.js +86 -0
  26. package/dist/contract/__fixtures__/http-client.generated.js.map +1 -0
  27. package/dist/contract/__fixtures__/http-convergence.d.ts +396 -0
  28. package/dist/contract/__fixtures__/http-convergence.d.ts.map +1 -0
  29. package/dist/contract/__fixtures__/http-convergence.js +425 -0
  30. package/dist/contract/__fixtures__/http-convergence.js.map +1 -0
  31. package/dist/contract/compiler/client.d.ts +12 -0
  32. package/dist/contract/compiler/client.d.ts.map +1 -0
  33. package/dist/contract/compiler/client.js +1293 -0
  34. package/dist/contract/compiler/client.js.map +1 -0
  35. package/dist/contract/compiler/index.d.ts +18 -0
  36. package/dist/contract/compiler/index.d.ts.map +1 -0
  37. package/dist/contract/compiler/index.js +1058 -0
  38. package/dist/contract/compiler/index.js.map +1 -0
  39. package/dist/contract/index.d.ts +237 -0
  40. package/dist/contract/index.d.ts.map +1 -0
  41. package/dist/contract/index.js +22 -0
  42. package/dist/contract/index.js.map +1 -0
  43. package/dist/csrf/index.d.ts +15 -0
  44. package/dist/csrf/index.d.ts.map +1 -0
  45. package/dist/csrf/index.js +218 -0
  46. package/dist/csrf/index.js.map +1 -0
  47. package/dist/data/index.d.ts +35 -0
  48. package/dist/data/index.d.ts.map +1 -0
  49. package/dist/data/index.js +58 -0
  50. package/dist/data/index.js.map +1 -0
  51. package/dist/devtools/index.d.ts +74 -0
  52. package/dist/devtools/index.d.ts.map +1 -0
  53. package/dist/devtools/index.js +552 -0
  54. package/dist/devtools/index.js.map +1 -0
  55. package/dist/dto-pipes/index.d.ts +45 -0
  56. package/dist/dto-pipes/index.d.ts.map +1 -0
  57. package/dist/dto-pipes/index.js +78 -0
  58. package/dist/dto-pipes/index.js.map +1 -0
  59. package/dist/gateways/index.d.ts +31 -0
  60. package/dist/gateways/index.d.ts.map +1 -0
  61. package/dist/gateways/index.js +138 -0
  62. package/dist/gateways/index.js.map +1 -0
  63. package/dist/health/index.d.ts +20 -0
  64. package/dist/health/index.d.ts.map +1 -0
  65. package/dist/health/index.js +148 -0
  66. package/dist/health/index.js.map +1 -0
  67. package/dist/http-token.d.ts +2 -0
  68. package/dist/http-token.d.ts.map +1 -0
  69. package/dist/http-token.js +21 -0
  70. package/dist/http-token.js.map +1 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +40 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/integrations/index.d.ts +11 -0
  76. package/dist/integrations/index.d.ts.map +1 -0
  77. package/dist/integrations/index.js +39 -0
  78. package/dist/integrations/index.js.map +1 -0
  79. package/dist/middleware/errors.d.ts +14 -0
  80. package/dist/middleware/errors.d.ts.map +1 -0
  81. package/dist/middleware/errors.js +18 -0
  82. package/dist/middleware/errors.js.map +1 -0
  83. package/dist/middleware/index.d.ts +46 -0
  84. package/dist/middleware/index.d.ts.map +1 -0
  85. package/dist/middleware/index.js +64 -0
  86. package/dist/middleware/index.js.map +1 -0
  87. package/dist/modules/__fixtures__/large-graph.d.ts +239 -0
  88. package/dist/modules/__fixtures__/large-graph.d.ts.map +1 -0
  89. package/dist/modules/__fixtures__/large-graph.js +483 -0
  90. package/dist/modules/__fixtures__/large-graph.js.map +1 -0
  91. package/dist/openapi/__fixtures__/entities.d.ts +8 -0
  92. package/dist/openapi/__fixtures__/entities.d.ts.map +1 -0
  93. package/dist/openapi/__fixtures__/entities.js +4 -0
  94. package/dist/openapi/__fixtures__/entities.js.map +1 -0
  95. package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts +38 -0
  96. package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts.map +1 -0
  97. package/dist/openapi/__fixtures__/openapi-tools.fixture.js +172 -0
  98. package/dist/openapi/__fixtures__/openapi-tools.fixture.js.map +1 -0
  99. package/dist/openapi/__fixtures__/route-schemas.d.ts +2 -0
  100. package/dist/openapi/__fixtures__/route-schemas.d.ts.map +1 -0
  101. package/dist/openapi/__fixtures__/route-schemas.js +17 -0
  102. package/dist/openapi/__fixtures__/route-schemas.js.map +1 -0
  103. package/dist/openapi/index.d.ts +61 -0
  104. package/dist/openapi/index.d.ts.map +1 -0
  105. package/dist/openapi/index.js +314 -0
  106. package/dist/openapi/index.js.map +1 -0
  107. package/dist/pipeline/guards.d.ts +9 -0
  108. package/dist/pipeline/guards.d.ts.map +1 -0
  109. package/dist/pipeline/guards.js +11 -0
  110. package/dist/pipeline/guards.js.map +1 -0
  111. package/dist/pipeline/index.d.ts +141 -0
  112. package/dist/pipeline/index.d.ts.map +1 -0
  113. package/dist/pipeline/index.js +1624 -0
  114. package/dist/pipeline/index.js.map +1 -0
  115. package/dist/routing/index.d.ts +40 -0
  116. package/dist/routing/index.d.ts.map +1 -0
  117. package/dist/routing/index.js +162 -0
  118. package/dist/routing/index.js.map +1 -0
  119. package/dist/static/index.d.ts +14 -0
  120. package/dist/static/index.d.ts.map +1 -0
  121. package/dist/static/index.js +348 -0
  122. package/dist/static/index.js.map +1 -0
  123. package/dist/testing/index.d.ts +20 -0
  124. package/dist/testing/index.d.ts.map +1 -0
  125. package/dist/testing/index.js +32 -0
  126. package/dist/testing/index.js.map +1 -0
  127. package/dist/traced-router.d.ts +5 -0
  128. package/dist/traced-router.d.ts.map +1 -0
  129. package/dist/traced-router.js +6 -0
  130. package/dist/traced-router.js.map +1 -0
  131. package/dist/upload/index.d.ts +28 -0
  132. package/dist/upload/index.d.ts.map +1 -0
  133. package/dist/upload/index.js +410 -0
  134. package/dist/upload/index.js.map +1 -0
  135. package/dist/versioning/index.d.ts +35 -0
  136. package/dist/versioning/index.d.ts.map +1 -0
  137. package/dist/versioning/index.js +66 -0
  138. package/dist/versioning/index.js.map +1 -0
  139. package/dist/versioning/runtime.d.ts +5 -0
  140. package/dist/versioning/runtime.d.ts.map +1 -0
  141. package/dist/versioning/runtime.js +34 -0
  142. package/dist/versioning/runtime.js.map +1 -0
  143. package/package.json +143 -0
  144. package/src/app/bridge.ts +39 -0
  145. package/src/app/index.ts +76 -0
  146. package/src/bench/index.ts +329 -0
  147. package/src/compression/index.ts +255 -0
  148. package/src/context/index.ts +181 -0
  149. package/src/contract/__fixtures__/http-client.generated.ts +73 -0
  150. package/src/contract/__fixtures__/http-client.generated.ts.map +1 -0
  151. package/src/contract/__fixtures__/http-convergence.ts +564 -0
  152. package/src/contract/compiler/client.ts +1518 -0
  153. package/src/contract/compiler/index.ts +1457 -0
  154. package/src/contract/index.ts +266 -0
  155. package/src/csrf/index.ts +264 -0
  156. package/src/data/index.ts +65 -0
  157. package/src/devtools/index.ts +727 -0
  158. package/src/dto-pipes/index.ts +93 -0
  159. package/src/gateways/index.ts +180 -0
  160. package/src/health/index.ts +198 -0
  161. package/src/http-token.ts +22 -0
  162. package/src/index.ts +167 -0
  163. package/src/integrations/index.ts +50 -0
  164. package/src/middleware/errors.ts +17 -0
  165. package/src/middleware/index.ts +108 -0
  166. package/src/modules/__fixtures__/large-graph.ts +540 -0
  167. package/src/openapi/__fixtures__/entities.ts +19 -0
  168. package/src/openapi/__fixtures__/openapi-tools.fixture.ts +212 -0
  169. package/src/openapi/__fixtures__/route-schemas.ts +27 -0
  170. package/src/openapi/index.ts +483 -0
  171. package/src/pipeline/guards.ts +23 -0
  172. package/src/pipeline/index.ts +2041 -0
  173. package/src/routing/index.ts +201 -0
  174. package/src/static/index.ts +443 -0
  175. package/src/testing/index.ts +58 -0
  176. package/src/traced-router.ts +8 -0
  177. package/src/upload/index.ts +489 -0
  178. package/src/versioning/index.ts +106 -0
  179. package/src/versioning/runtime.ts +38 -0
@@ -0,0 +1,540 @@
1
+ // The one realistic fixture the graph tests read. Tests freeze for the epic "The module graph as
2
+ // a first-class object" (#598 / spec freeze #599); the frozen text is `../SPEC.md`'s
3
+ // `## Amendments (lazy modules and the graph's data source, #599)` and `../../devtools/SPEC.md`.
4
+ //
5
+ // One fixture rather than several toy ones, because a graph description is trivially correct for
6
+ // three providers and interesting for thirty: `AppModule` below is seven modules deep with mixed
7
+ // scopes, a provider whose only consumer is another provider's factory, two controllers sharing a
8
+ // token, a route that shadows another, and one lazily imported subtree. Every variant a finding
9
+ // test needs is a separate root exported from the same file so the shapes stay comparable.
10
+ //
11
+ // `__fixtures__/` is not reachable from any `exports` target, which is what makes it the one place
12
+ // under `packages/web/src` where a decorator may be *applied* (ARCHITECTURE.md directive 10 forbids
13
+ // that only on a path reachable from an entry point). `vitest.config.ts`'s `stage3Decorators()`
14
+ // plugin lowers any `.ts` file containing one, not only `*.spec.ts`, so this file runs under the
15
+ // suite unchanged.
16
+
17
+ import { Inject, createToken, type Container, type Token } from '@zmdb/app/di';
18
+ import { lazy, Module, type ModuleClass, type ProviderDef } from '@zmdb/app/modules';
19
+
20
+ import { Controller, Delete, Get, Post } from '../../routing/index.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Tokens
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export const CONFIG = createToken<{ readonly url: string }>('CONFIG');
27
+ export const CLOCK = createToken<{ now(): number }>('CLOCK');
28
+ export const REQUEST_ID = createToken<{ readonly id: number }>('REQUEST_ID');
29
+ /** A space and a `#` in one description — `../../devtools/SPEC.md` §11.9's DOT quoting case. */
30
+ export const USER_CACHE = createToken<Map<string, string>>('user cache #1');
31
+ export const POOL = createToken<{ readonly dsn: string }>('POOL');
32
+ export const USERS_REPOSITORY = createToken<{ find(id: string): string }>('USERS_REPOSITORY');
33
+ export const INVOICES = createToken<{ total(): number }>('INVOICES');
34
+ export const SEARCH_INDEX = createToken<{ readonly size: number }>('SEARCH_INDEX');
35
+ export const ADMIN_POOL = createToken<{ readonly dsn: string }>('ADMIN_POOL');
36
+
37
+ /** Every factory call the fixture makes, so a test can say which ran and how often. */
38
+ export const factoryCalls: string[] = [];
39
+
40
+ /** Every lifecycle hook the fixture runs, in order, so a test can say which ran and when. */
41
+ export const hookLog: string[] = [];
42
+
43
+ export function resetFactoryCalls(): void {
44
+ factoryCalls.length = 0;
45
+ hookLog.length = 0;
46
+ }
47
+
48
+ /** A promise a test resolves by hand, so a load can be held in flight across an `await`. */
49
+ export function gate(): { readonly promise: Promise<void>; open(): void } {
50
+ let open = (): void => {};
51
+ const promise = new Promise<void>(resolve => {
52
+ open = resolve;
53
+ });
54
+ return { promise, open: () => open() };
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // The application graph
59
+ // ---------------------------------------------------------------------------
60
+
61
+ @Module({
62
+ providers: [
63
+ { token: CONFIG, useValue: { url: 'postgres://localhost/app' } },
64
+ {
65
+ token: CLOCK,
66
+ useFactory: () => {
67
+ factoryCalls.push('CLOCK');
68
+ return { now: () => 0 };
69
+ },
70
+ },
71
+ {
72
+ token: REQUEST_ID,
73
+ useFactory: () => {
74
+ factoryCalls.push('REQUEST_ID');
75
+ return { id: factoryCalls.length };
76
+ },
77
+ scope: 'transient',
78
+ },
79
+ { token: USER_CACHE, useValue: new Map<string, string>() },
80
+ ],
81
+ exports: [CONFIG, CLOCK],
82
+ })
83
+ export class CoreModule {}
84
+
85
+ @Module({
86
+ imports: [CoreModule],
87
+ providers: [
88
+ {
89
+ token: POOL,
90
+ useFactory: (container: Container) => {
91
+ factoryCalls.push('POOL');
92
+ return { dsn: container.resolve(CONFIG).url };
93
+ },
94
+ },
95
+ {
96
+ // The edge no description can see: `USERS_REPOSITORY` resolves `POOL` inside the factory
97
+ // body, so `POOL`'s only consumer is opaque (`../../devtools/SPEC.md` §2, §11.5).
98
+ token: USERS_REPOSITORY,
99
+ useFactory: (container: Container) => {
100
+ factoryCalls.push('USERS_REPOSITORY');
101
+ const pool = container.resolve(POOL);
102
+ return { find: (id: string) => `${pool.dsn}#${id}` };
103
+ },
104
+ },
105
+ ],
106
+ exports: [USERS_REPOSITORY],
107
+ })
108
+ export class DataModule {}
109
+
110
+ @Controller('/users')
111
+ export class UsersController {
112
+ @Inject(USERS_REPOSITORY)
113
+ repository!: { find(id: string): string };
114
+
115
+ @Inject(CONFIG)
116
+ config!: { readonly url: string };
117
+
118
+ // `/users/:id` is declared before `/users/me`, so it shadows it under first-match routing —
119
+ // the case `docs-site/content/web-devtools.md` says a route printout makes visible.
120
+ @Get('/:id')
121
+ byId(): string {
122
+ return 'byId';
123
+ }
124
+
125
+ @Get('/me')
126
+ me(): string {
127
+ return 'me';
128
+ }
129
+
130
+ @Post('/')
131
+ create(): string {
132
+ return 'created';
133
+ }
134
+ }
135
+
136
+ @Module({ imports: [DataModule], controllers: [UsersController] })
137
+ export class UsersModule {}
138
+
139
+ @Controller('/invoices')
140
+ export class BillingController {
141
+ // The second consumer of `USERS_REPOSITORY`, which is what `dependentsOf` has to report both of.
142
+ @Inject(USERS_REPOSITORY)
143
+ repository!: { find(id: string): string };
144
+
145
+ @Inject(INVOICES)
146
+ invoices!: { total(): number };
147
+
148
+ @Get('/:id')
149
+ byId(): string {
150
+ return 'invoice';
151
+ }
152
+ }
153
+
154
+ @Module({
155
+ imports: [DataModule],
156
+ providers: [
157
+ {
158
+ token: INVOICES,
159
+ useFactory: () => {
160
+ factoryCalls.push('INVOICES');
161
+ return { total: () => 0 };
162
+ },
163
+ },
164
+ ],
165
+ controllers: [BillingController],
166
+ })
167
+ export class BillingModule {}
168
+
169
+ @Controller('/search')
170
+ export class SearchController {
171
+ @Inject(SEARCH_INDEX)
172
+ index!: { readonly size: number };
173
+
174
+ @Get('/')
175
+ query(): string {
176
+ return 'search';
177
+ }
178
+ }
179
+
180
+ @Module({
181
+ imports: [CoreModule],
182
+ providers: [
183
+ {
184
+ token: SEARCH_INDEX,
185
+ useFactory: () => {
186
+ factoryCalls.push('SEARCH_INDEX');
187
+ return { size: 0 };
188
+ },
189
+ scope: 'transient',
190
+ },
191
+ ],
192
+ controllers: [SearchController],
193
+ })
194
+ export class SearchModule {}
195
+
196
+ /** The lazily imported module's controller. It carries all three hooks, for §L12.8 and §L12.11. */
197
+ @Controller('/admin')
198
+ export class AdminController {
199
+ @Inject(ADMIN_POOL)
200
+ pool!: { readonly dsn: string };
201
+
202
+ onModuleInit(): void {
203
+ hookLog.push('AdminController.onModuleInit');
204
+ }
205
+
206
+ onApplicationBootstrap(): void {
207
+ hookLog.push('AdminController.onApplicationBootstrap');
208
+ }
209
+
210
+ onShutdown(): void {
211
+ hookLog.push('AdminController.onShutdown');
212
+ }
213
+
214
+ @Get('/')
215
+ list(): string {
216
+ hookLog.push('AdminController.list');
217
+ return 'admin';
218
+ }
219
+
220
+ @Delete('/:id')
221
+ remove(): string {
222
+ return 'removed';
223
+ }
224
+ }
225
+
226
+ @Module({
227
+ providers: [
228
+ {
229
+ token: ADMIN_POOL,
230
+ useFactory: () => {
231
+ factoryCalls.push('ADMIN_POOL');
232
+ return { dsn: 'postgres://localhost/admin' };
233
+ },
234
+ },
235
+ ],
236
+ controllers: [AdminController],
237
+ })
238
+ export class AdminModule {}
239
+
240
+ /** The eager controller the lazy one has to tear down before (§L12.11). */
241
+ @Controller('/health')
242
+ export class HealthController {
243
+ @Inject(CLOCK)
244
+ clock!: { now(): number };
245
+
246
+ onModuleInit(): void {
247
+ hookLog.push('HealthController.onModuleInit');
248
+ }
249
+
250
+ onApplicationBootstrap(): void {
251
+ hookLog.push('HealthController.onApplicationBootstrap');
252
+ }
253
+
254
+ onShutdown(): void {
255
+ hookLog.push('HealthController.onShutdown');
256
+ }
257
+
258
+ @Get('/')
259
+ check(): string {
260
+ return 'ok';
261
+ }
262
+ }
263
+
264
+ /** The fixture root: three eager subtrees, one lazy one, and a controller of its own. */
265
+ @Module({
266
+ imports: [UsersModule, BillingModule, SearchModule, lazy(AdminModule)],
267
+ controllers: [HealthController],
268
+ })
269
+ export class AppModule {}
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Variants, one per finding and one per lazy rule
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /**
276
+ * A real three-module cycle.
277
+ *
278
+ * The closing edge is pushed after all three classes exist because it cannot be written any other
279
+ * way: a decorator expression is evaluated before its class binding is initialised, so
280
+ * `imports: [CycleAppModule]` on the module `CycleAppModule` transitively imports is a
281
+ * `ReferenceError` at definition time. `def.imports` holds this array by reference, so mutating it
282
+ * afterwards is what a cycle across two files looks like from `compileModule`'s side — except for
283
+ * one difference recorded in the tests: a genuine ESM import cycle leaves `undefined` in the array
284
+ * instead, which is a different failure.
285
+ */
286
+ const closingEdge: ModuleClass[] = [];
287
+
288
+ @Module({ imports: closingEdge })
289
+ export class CycleUsersModule {}
290
+
291
+ @Module({ imports: [CycleUsersModule] })
292
+ export class CycleBillingModule {}
293
+
294
+ @Module({ imports: [CycleBillingModule] })
295
+ export class CycleAppModule {}
296
+
297
+ closingEdge.push(CycleAppModule);
298
+
299
+ /** The ESM shape of the same mistake: the forward reference is `undefined` at decoration time. */
300
+ const undefinedEdge: readonly ModuleClass[] = [undefined as unknown as ModuleClass];
301
+
302
+ @Module({ imports: undefinedEdge })
303
+ export class UninitialisedImportModule {}
304
+
305
+ /** Two controllers registering `GET /reports/:id`. The first declared wins; §5's `shadowed-route`. */
306
+ @Controller('/reports')
307
+ export class ReportsController {
308
+ @Get('/:id')
309
+ byId(): string {
310
+ return 'first';
311
+ }
312
+ }
313
+
314
+ @Controller('/reports')
315
+ export class ArchivedReportsController {
316
+ @Get('/:id')
317
+ byId(): string {
318
+ return 'second';
319
+ }
320
+ }
321
+
322
+ @Module({ controllers: [ReportsController, ArchivedReportsController] })
323
+ export class ShadowedRouteAppModule {}
324
+
325
+ /** Two modules registering `CONFIG`; §5's `duplicate-provider` finding and startup refusal. */
326
+ @Module({ providers: [{ token: CONFIG, useValue: { url: 'first' } }] })
327
+ export class FirstConfigModule {}
328
+
329
+ @Module({ providers: [{ token: CONFIG, useValue: { url: 'second' } }] })
330
+ export class SecondConfigModule {}
331
+
332
+ @Module({ imports: [FirstConfigModule, SecondConfigModule] })
333
+ export class DuplicateProviderAppModule {}
334
+
335
+ /** Two distinct tokens with one description. `createToken` derives no identity from it; §5's warning. */
336
+ export const DB_PRIMARY = createToken<string>('db');
337
+ export const DB_REPLICA = createToken<string>('db');
338
+
339
+ @Module({
340
+ providers: [
341
+ { token: DB_PRIMARY, useValue: 'primary' },
342
+ { token: DB_REPLICA, useValue: 'replica' },
343
+ ],
344
+ })
345
+ export class AmbiguousTokenAppModule {}
346
+
347
+ /** A controller injecting a token no module registers; §5's `unresolved-token`. */
348
+ export const NEVER_REGISTERED = createToken<string>('NEVER_REGISTERED');
349
+
350
+ @Controller('/missing')
351
+ export class NeedsMissingController {
352
+ @Inject(NEVER_REGISTERED)
353
+ missing!: string;
354
+
355
+ @Get('/')
356
+ read(): string {
357
+ return this.missing;
358
+ }
359
+ }
360
+
361
+ @Module({ controllers: [NeedsMissingController] })
362
+ export class UnresolvedTokenAppModule {}
363
+
364
+ /**
365
+ * The unresolved-token variant with a provider that *would* run first, so a test can say whether
366
+ * anything was constructed before the refusal (§L12.6: "before any provider is constructed").
367
+ */
368
+ @Module({
369
+ imports: [CoreModule],
370
+ controllers: [HealthController],
371
+ })
372
+ export class WarmModule {}
373
+
374
+ @Module({ imports: [WarmModule, UnresolvedTokenAppModule] })
375
+ export class UnresolvedAfterWarmAppModule {}
376
+
377
+ /** An eager controller injecting a token only the lazily imported module provides; §L3's last row. */
378
+ @Controller('/eager')
379
+ export class EagerNeedsAdminController {
380
+ @Inject(ADMIN_POOL)
381
+ pool!: { readonly dsn: string };
382
+
383
+ @Get('/')
384
+ read(): string {
385
+ return this.pool.dsn;
386
+ }
387
+ }
388
+
389
+ @Module({
390
+ imports: [lazy(AdminModule)],
391
+ controllers: [EagerNeedsAdminController],
392
+ })
393
+ export class EagerDependsOnLazyAppModule {}
394
+
395
+ /** `Shared` reached lazily from one importer and eagerly from another; §L3 makes it eager. */
396
+ export const SHARED = createToken<{ readonly n: number }>('SHARED');
397
+
398
+ @Module({
399
+ providers: [
400
+ {
401
+ token: SHARED,
402
+ useFactory: () => {
403
+ factoryCalls.push('SHARED');
404
+ return { n: 1 };
405
+ },
406
+ },
407
+ ],
408
+ })
409
+ export class SharedModule {}
410
+
411
+ @Module({ imports: [lazy(SharedModule)] })
412
+ export class LazyImporterModule {}
413
+
414
+ @Module({ imports: [SharedModule] })
415
+ export class EagerImporterModule {}
416
+
417
+ @Module({ imports: [LazyImporterModule, EagerImporterModule] })
418
+ export class LazyThenEagerAppModule {}
419
+
420
+ @Module({ imports: [EagerImporterModule, LazyImporterModule] })
421
+ export class EagerThenLazyAppModule {}
422
+
423
+ /**
424
+ * A lazy subtree whose `useFactory` throws — §L7's terminal failure, §L12.9 and §L12.10.
425
+ *
426
+ * The throw is in a factory rather than a constructor because a factory is the case §L7 argues
427
+ * from (the socket that is already open when the next line throws), and because `factoryCalls`
428
+ * makes "ran exactly once" a countable claim rather than an inference.
429
+ */
430
+ export const BROKEN_POOL = createToken<{ readonly dsn: string }>('BROKEN_POOL');
431
+
432
+ @Controller('/broken')
433
+ export class BrokenController {
434
+ @Inject(BROKEN_POOL)
435
+ pool!: { readonly dsn: string };
436
+
437
+ @Get('/')
438
+ read(): string {
439
+ return this.pool.dsn;
440
+ }
441
+ }
442
+
443
+ @Module({
444
+ providers: [
445
+ {
446
+ token: BROKEN_POOL,
447
+ useFactory: () => {
448
+ factoryCalls.push('BROKEN_POOL');
449
+ throw new Error('fixture: the pool could not be opened');
450
+ },
451
+ },
452
+ ],
453
+ controllers: [BrokenController],
454
+ })
455
+ export class BrokenModule {}
456
+
457
+ @Module({ imports: [CoreModule, lazy(BrokenModule)], controllers: [HealthController] })
458
+ export class BrokenLazyAppModule {}
459
+
460
+ /**
461
+ * A lazy subtree whose load cannot finish until a test opens the gate, so `status` can be observed
462
+ * at `'loading'` and `[Symbol.asyncDispose]` can be called mid-load (§L8, §L12.11).
463
+ *
464
+ * The gate is in `onModuleInit` and not in the factory on purpose: §L5 freezes wiring as one
465
+ * synchronous region with the hooks as the only `await`, so a hook is the only place a load is
466
+ * legitimately suspendable. A gate in a `useFactory` would be testing a shape the spec forbids.
467
+ */
468
+ export const slowGate = gate();
469
+
470
+ @Controller('/slow')
471
+ export class SlowController {
472
+ async onModuleInit(): Promise<void> {
473
+ hookLog.push('SlowController.onModuleInit:enter');
474
+ await slowGate.promise;
475
+ hookLog.push('SlowController.onModuleInit:exit');
476
+ }
477
+
478
+ @Get('/')
479
+ read(): string {
480
+ return 'slow';
481
+ }
482
+ }
483
+
484
+ @Module({ controllers: [SlowController] })
485
+ export class SlowModule {}
486
+
487
+ @Module({ imports: [CoreModule, lazy(SlowModule)], controllers: [HealthController] })
488
+ export class SlowLazyAppModule {}
489
+
490
+ /** A root with no `lazy()` anywhere, for the "costs nothing when unused" half of §L12.5. */
491
+ @Module({ imports: [UsersModule, BillingModule, SearchModule], controllers: [HealthController] })
492
+ export class EagerOnlyAppModule {}
493
+
494
+ /** A module whose `name` is empty, standing in for a minified build; §5's `anonymous-class` warning. */
495
+ @Module({})
496
+ export class MangledModule {}
497
+
498
+ Object.defineProperty(MangledModule, 'name', { value: '' });
499
+
500
+ /**
501
+ * Sixty value providers from one array, which is the fixture `--providers` has to refuse
502
+ * unfiltered (`../../devtools/SPEC.md` §8's fifty-node threshold). Providers are data, so the
503
+ * count needs no sixty class declarations.
504
+ */
505
+ export const bulkTokens: readonly Token<number>[] = Array.from({ length: 60 }, (_, index) =>
506
+ createToken<number>(`bulk.${index}`),
507
+ );
508
+
509
+ const bulkProviders: readonly ProviderDef[] = bulkTokens.map((token, index) => ({ token, useValue: index }));
510
+
511
+ @Module({ providers: bulkProviders })
512
+ export class WideModule {}
513
+
514
+ @Module({ imports: [WideModule, UsersModule] })
515
+ export class WideAppModule {}
516
+
517
+ /** A controller subclassing a controller — `../../devtools/SPEC.md` §4's prototype-aliasing case. */
518
+ @Controller('/base')
519
+ export class BaseInjectController {
520
+ @Inject(CONFIG)
521
+ config!: { readonly url: string };
522
+
523
+ @Get('/a')
524
+ a(): string {
525
+ return 'a';
526
+ }
527
+ }
528
+
529
+ export class DerivedInjectController extends BaseInjectController {
530
+ @Inject(CLOCK)
531
+ clock!: { now(): number };
532
+
533
+ @Get('/b')
534
+ b(): string {
535
+ return 'b';
536
+ }
537
+ }
538
+
539
+ @Module({ imports: [CoreModule], controllers: [BaseInjectController, DerivedInjectController] })
540
+ export class SubclassedControllerAppModule {}
@@ -0,0 +1,19 @@
1
+ // The entity the route documents are derived from. A separate file so `route-schemas.ts`
2
+ // contains nothing but imports and the one call the spec evaluates.
3
+
4
+ import {
5
+ type HasDefault,
6
+ type Length,
7
+ type PrimaryKey,
8
+ type Sensitive,
9
+ type Serial,
10
+ type Sql,
11
+ type Table,
12
+ } from '@zmdb/schema/tags';
13
+
14
+ export interface User extends Table<'users'> {
15
+ id: number & Sql<'integer'> & Serial & PrimaryKey;
16
+ email: string & Sql<'varchar'> & Length<255>;
17
+ createdAt: Date & Sql<'timestamp'> & HasDefault;
18
+ passwordHash: string & Sql<'text'> & Sensitive;
19
+ }