@geekmidas/constructs 7.0.1 → 9.1.0

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 (186) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/{AWSScheduledFunction-CTIa90EH.d.cts → AWSScheduledFunction-BHHlWlfV.d.cts} +2 -2
  3. package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts.map → AWSScheduledFunction-BHHlWlfV.d.cts.map} +1 -1
  4. package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts → AWSScheduledFunction-BzGCUbkk.d.mts} +2 -2
  5. package/dist/{AWSScheduledFunction-CTIa90EH.d.cts.map → AWSScheduledFunction-BzGCUbkk.d.mts.map} +1 -1
  6. package/dist/{AWSScheduledFunction-DbkDr8CA.cjs → AWSScheduledFunction-C42adp8A.cjs} +16 -5
  7. package/dist/AWSScheduledFunction-C42adp8A.cjs.map +1 -0
  8. package/dist/{AWSScheduledFunction-CufMMhGB.mjs → AWSScheduledFunction-KP6lqbsW.mjs} +15 -4
  9. package/dist/AWSScheduledFunction-KP6lqbsW.mjs.map +1 -0
  10. package/dist/{Construct-r0U2zBwW.mjs → Construct-BWF5HxaM.mjs} +5 -2
  11. package/dist/Construct-BWF5HxaM.mjs.map +1 -0
  12. package/dist/{Construct-_1wX7p_Y.d.cts → Construct-JlyQkBkv.d.cts} +39 -4
  13. package/dist/Construct-JlyQkBkv.d.cts.map +1 -0
  14. package/dist/{Construct-vXMZJgRp.cjs → Construct-TGIM_E4o.cjs} +5 -2
  15. package/dist/Construct-TGIM_E4o.cjs.map +1 -0
  16. package/dist/{Construct-DCMuYOox.d.mts → Construct-pxwC6fDD.d.mts} +39 -4
  17. package/dist/Construct-pxwC6fDD.d.mts.map +1 -0
  18. package/dist/{Endpoint-CLPSIl6S.d.mts → Endpoint-B0P_pEMr.d.mts} +3 -3
  19. package/dist/{Endpoint-CLPSIl6S.d.mts.map → Endpoint-B0P_pEMr.d.mts.map} +1 -1
  20. package/dist/{Endpoint-CAzNbDEt.d.cts → Endpoint-Cwjhc1Hl.d.cts} +2 -2
  21. package/dist/Endpoint-Cwjhc1Hl.d.cts.map +1 -0
  22. package/dist/{HonoEndpointAdaptor-DJDT3PMx.mjs → HonoEndpointAdaptor-BMVwx2cz.mjs} +18 -11
  23. package/dist/HonoEndpointAdaptor-BMVwx2cz.mjs.map +1 -0
  24. package/dist/{HonoEndpointAdaptor-CkJENANP.cjs → HonoEndpointAdaptor-DPDsDi-D.cjs} +19 -12
  25. package/dist/HonoEndpointAdaptor-DPDsDi-D.cjs.map +1 -0
  26. package/dist/{Queue-HBDf0fqz.d.mts → Queue-BL68oDJ-.d.mts} +2 -2
  27. package/dist/{Queue-DaxjwmOQ.d.cts.map → Queue-BL68oDJ-.d.mts.map} +1 -1
  28. package/dist/{Queue-DaxjwmOQ.d.cts → Queue-DxJ6btRE.d.cts} +2 -2
  29. package/dist/{Queue-HBDf0fqz.d.mts.map → Queue-DxJ6btRE.d.cts.map} +1 -1
  30. package/dist/{Subscriber-B69ngPwl.d.cts → Subscriber-BpVhcGhX.d.cts} +2 -2
  31. package/dist/{Subscriber-B69ngPwl.d.cts.map → Subscriber-BpVhcGhX.d.cts.map} +1 -1
  32. package/dist/{Subscriber-D8Xm-86W.d.mts → Subscriber-DF6xsBAJ.d.mts} +2 -2
  33. package/dist/{Subscriber-D8Xm-86W.d.mts.map → Subscriber-DF6xsBAJ.d.mts.map} +1 -1
  34. package/dist/{Topic-DT10AqHk.d.mts → Topic-DJXoDeMP.d.cts} +3 -3
  35. package/dist/{Topic-C5ZMfveW.d.cts.map → Topic-DJXoDeMP.d.cts.map} +1 -1
  36. package/dist/{Topic-C5ZMfveW.d.cts → Topic-DxWQBgMN.d.mts} +3 -3
  37. package/dist/{Topic-DT10AqHk.d.mts.map → Topic-DxWQBgMN.d.mts.map} +1 -1
  38. package/dist/adaptors/aws.cjs +14 -7
  39. package/dist/adaptors/aws.cjs.map +1 -1
  40. package/dist/adaptors/aws.d.cts +6 -6
  41. package/dist/adaptors/aws.d.cts.map +1 -1
  42. package/dist/adaptors/aws.d.mts +8 -8
  43. package/dist/adaptors/aws.d.mts.map +1 -1
  44. package/dist/adaptors/aws.mjs +14 -7
  45. package/dist/adaptors/aws.mjs.map +1 -1
  46. package/dist/adaptors/hono.cjs +7 -6
  47. package/dist/adaptors/hono.d.cts +3 -3
  48. package/dist/adaptors/hono.d.cts.map +1 -1
  49. package/dist/adaptors/hono.d.mts +4 -4
  50. package/dist/adaptors/hono.d.mts.map +1 -1
  51. package/dist/adaptors/hono.mjs +7 -6
  52. package/dist/adaptors/testing.cjs +161 -126
  53. package/dist/adaptors/testing.cjs.map +1 -1
  54. package/dist/adaptors/testing.d.cts +7 -5
  55. package/dist/adaptors/testing.d.cts.map +1 -1
  56. package/dist/adaptors/testing.d.mts +8 -6
  57. package/dist/adaptors/testing.d.mts.map +1 -1
  58. package/dist/adaptors/testing.mjs +162 -127
  59. package/dist/adaptors/testing.mjs.map +1 -1
  60. package/dist/clone-I_WIpiFf.cjs +45 -0
  61. package/dist/clone-I_WIpiFf.cjs.map +1 -0
  62. package/dist/clone-pdXxc0t3.mjs +39 -0
  63. package/dist/clone-pdXxc0t3.mjs.map +1 -0
  64. package/dist/crons/index.cjs +14 -32
  65. package/dist/crons/index.cjs.map +1 -1
  66. package/dist/crons/index.d.cts +7 -7
  67. package/dist/crons/index.d.cts.map +1 -1
  68. package/dist/crons/index.d.mts +3 -3
  69. package/dist/crons/index.d.mts.map +1 -1
  70. package/dist/crons/index.mjs +14 -32
  71. package/dist/crons/index.mjs.map +1 -1
  72. package/dist/endpoints/index.cjs +74 -61
  73. package/dist/endpoints/index.cjs.map +1 -1
  74. package/dist/endpoints/index.d.cts +14 -6
  75. package/dist/endpoints/index.d.cts.map +1 -1
  76. package/dist/endpoints/index.d.mts +13 -5
  77. package/dist/endpoints/index.d.mts.map +1 -1
  78. package/dist/endpoints/index.mjs +74 -61
  79. package/dist/endpoints/index.mjs.map +1 -1
  80. package/dist/functions/index.cjs +3 -2
  81. package/dist/functions/index.d.cts +2 -2
  82. package/dist/functions/index.d.mts +2 -2
  83. package/dist/functions/index.mjs +3 -2
  84. package/dist/{functions-B-qt91O7.mjs → functions-EqTloHjo.mjs} +107 -46
  85. package/dist/functions-EqTloHjo.mjs.map +1 -0
  86. package/dist/{functions-C8i1kCMM.cjs → functions-SqtCDc9c.cjs} +118 -45
  87. package/dist/functions-SqtCDc9c.cjs.map +1 -0
  88. package/dist/{index-DP6fqv0I.d.mts → index-C7iynzLI.d.mts} +8 -2
  89. package/dist/index-C7iynzLI.d.mts.map +1 -0
  90. package/dist/{index-BGeTScmi.d.cts → index-DdgJRm4q.d.cts} +12 -6
  91. package/dist/index-DdgJRm4q.d.cts.map +1 -0
  92. package/dist/index.cjs +1 -1
  93. package/dist/index.d.cts +2 -2
  94. package/dist/index.d.mts +4 -4
  95. package/dist/index.mjs +1 -1
  96. package/dist/{lazyAccessors-DfqZR6C4.mjs → lazyAccessors-CCTK19xj.mjs} +3 -3
  97. package/dist/{lazyAccessors-DfqZR6C4.mjs.map → lazyAccessors-CCTK19xj.mjs.map} +1 -1
  98. package/dist/{lazyAccessors-DmTG8sQH.cjs → lazyAccessors-D04oftGw.cjs} +3 -3
  99. package/dist/{lazyAccessors-DmTG8sQH.cjs.map → lazyAccessors-D04oftGw.cjs.map} +1 -1
  100. package/dist/monitors/index.cjs +49 -0
  101. package/dist/monitors/index.cjs.map +1 -0
  102. package/dist/monitors/index.d.cts +45 -0
  103. package/dist/monitors/index.d.cts.map +1 -0
  104. package/dist/monitors/index.d.mts +45 -0
  105. package/dist/monitors/index.d.mts.map +1 -0
  106. package/dist/monitors/index.mjs +48 -0
  107. package/dist/monitors/index.mjs.map +1 -0
  108. package/dist/{processAudits-CKjjivyz.cjs → processAudits-BJZT5gmy.cjs} +2 -2
  109. package/dist/{processAudits-CKjjivyz.cjs.map → processAudits-BJZT5gmy.cjs.map} +1 -1
  110. package/dist/{processAudits-ySnizdMG.mjs → processAudits-CwyaaSQc.mjs} +1 -1
  111. package/dist/{processAudits-ySnizdMG.mjs.map → processAudits-CwyaaSQc.mjs.map} +1 -1
  112. package/dist/{publisher-lFQleddL.cjs → publisher-BZgcrVPy.cjs} +1 -1
  113. package/dist/{publisher-lFQleddL.cjs.map → publisher-BZgcrVPy.cjs.map} +1 -1
  114. package/dist/{publisher-BrFOF847.mjs → publisher-CnpeMv-r.mjs} +1 -1
  115. package/dist/{publisher-BrFOF847.mjs.map → publisher-CnpeMv-r.mjs.map} +1 -1
  116. package/dist/queue/index.cjs +9 -22
  117. package/dist/queue/index.cjs.map +1 -1
  118. package/dist/queue/index.d.cts +4 -4
  119. package/dist/queue/index.d.cts.map +1 -1
  120. package/dist/queue/index.d.mts +2 -2
  121. package/dist/queue/index.d.mts.map +1 -1
  122. package/dist/queue/index.mjs +9 -22
  123. package/dist/queue/index.mjs.map +1 -1
  124. package/dist/subscribers/index.cjs +9 -22
  125. package/dist/subscribers/index.cjs.map +1 -1
  126. package/dist/subscribers/index.d.cts +5 -5
  127. package/dist/subscribers/index.d.cts.map +1 -1
  128. package/dist/subscribers/index.d.mts +3 -3
  129. package/dist/subscribers/index.d.mts.map +1 -1
  130. package/dist/subscribers/index.mjs +9 -22
  131. package/dist/subscribers/index.mjs.map +1 -1
  132. package/dist/{telemetry-3vdxGn4q.d.mts → telemetry-C--EgYVr.d.mts} +1 -1
  133. package/dist/{telemetry-3vdxGn4q.d.mts.map → telemetry-C--EgYVr.d.mts.map} +1 -1
  134. package/dist/topic/index.cjs +4 -7
  135. package/dist/topic/index.cjs.map +1 -1
  136. package/dist/topic/index.d.cts +2 -2
  137. package/dist/topic/index.d.cts.map +1 -1
  138. package/dist/topic/index.d.mts +2 -2
  139. package/dist/topic/index.d.mts.map +1 -1
  140. package/dist/topic/index.mjs +4 -7
  141. package/dist/topic/index.mjs.map +1 -1
  142. package/dist/{types-BDNGr7pv.d.mts → types-DNngH0fX.d.mts} +1 -1
  143. package/dist/{types-BDNGr7pv.d.mts.map → types-DNngH0fX.d.mts.map} +1 -1
  144. package/dist/types.d.mts +1 -1
  145. package/package.json +32 -22
  146. package/src/Construct.ts +5 -0
  147. package/src/__tests__/builder-reuse.spec.ts +587 -0
  148. package/src/__tests__/monitor.spec.ts +636 -0
  149. package/src/clone.ts +38 -0
  150. package/src/crons/CronBuilder.ts +35 -45
  151. package/src/crons/__tests__/Cron.spec.ts +10 -2
  152. package/src/crons/__tests__/CronBuilder.state-isolation.spec.ts +64 -18
  153. package/src/endpoints/AmazonApiGatewayEndpointAdaptor.ts +23 -2
  154. package/src/endpoints/EndpointBuilder.ts +61 -70
  155. package/src/endpoints/EndpointFactory.ts +31 -0
  156. package/src/endpoints/HonoEndpointAdaptor.ts +16 -1
  157. package/src/endpoints/TestEndpointAdaptor.ts +194 -150
  158. package/src/endpoints/__tests__/EndpointBuilder.audit.spec.ts +6 -3
  159. package/src/endpoints/__tests__/EndpointBuilder.responseType.spec.ts +4 -1
  160. package/src/endpoints/__tests__/EndpointBuilder.spec.ts +16 -8
  161. package/src/functions/AWSLambdaFunction.ts +28 -2
  162. package/src/functions/BaseFunctionBuilder.ts +33 -22
  163. package/src/functions/FunctionBuilder.ts +41 -51
  164. package/src/functions/TestFunctionAdaptor.ts +66 -31
  165. package/src/functions/__tests__/FunctionBuilder.state-isolation.spec.ts +55 -12
  166. package/src/index.ts +6 -0
  167. package/src/monitor.ts +145 -0
  168. package/src/monitors/heartbeat.ts +86 -0
  169. package/src/monitors/index.ts +6 -0
  170. package/src/queue/QueueBuilder.ts +22 -22
  171. package/src/subscribers/SubscriberBuilder.ts +19 -22
  172. package/src/topic/TopicBuilder.ts +9 -7
  173. package/tsdown.config.ts +1 -0
  174. package/dist/AWSScheduledFunction-CufMMhGB.mjs.map +0 -1
  175. package/dist/AWSScheduledFunction-DbkDr8CA.cjs.map +0 -1
  176. package/dist/Construct-DCMuYOox.d.mts.map +0 -1
  177. package/dist/Construct-_1wX7p_Y.d.cts.map +0 -1
  178. package/dist/Construct-r0U2zBwW.mjs.map +0 -1
  179. package/dist/Construct-vXMZJgRp.cjs.map +0 -1
  180. package/dist/Endpoint-CAzNbDEt.d.cts.map +0 -1
  181. package/dist/HonoEndpointAdaptor-CkJENANP.cjs.map +0 -1
  182. package/dist/HonoEndpointAdaptor-DJDT3PMx.mjs.map +0 -1
  183. package/dist/functions-B-qt91O7.mjs.map +0 -1
  184. package/dist/functions-C8i1kCMM.cjs.map +0 -1
  185. package/dist/index-BGeTScmi.d.cts.map +0 -1
  186. package/dist/index-DP6fqv0I.d.mts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/constructs",
3
- "version": "7.0.1",
3
+ "version": "9.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -54,6 +54,16 @@
54
54
  "default": "./dist/topic/index.cjs"
55
55
  }
56
56
  },
57
+ "./monitors": {
58
+ "import": {
59
+ "types": "./dist/monitors/index.d.mts",
60
+ "default": "./dist/monitors/index.mjs"
61
+ },
62
+ "require": {
63
+ "types": "./dist/monitors/index.d.cts",
64
+ "default": "./dist/monitors/index.cjs"
65
+ }
66
+ },
57
67
  "./crons": {
58
68
  "import": {
59
69
  "types": "./dist/crons/index.d.mts",
@@ -146,33 +156,33 @@
146
156
  "pg": "~8.16.3",
147
157
  "msw": "~2.10.3",
148
158
  "zod": "~4.1.13",
149
- "@geekmidas/audit": "^2.2.1",
150
- "@geekmidas/cache": "^1.1.2",
151
- "@geekmidas/db": "^1.1.1",
152
- "@geekmidas/envkit": "^1.1.1",
153
- "@geekmidas/errors": "^1.0.2",
154
- "@geekmidas/events": "^1.1.6",
155
- "@geekmidas/logger": "^1.0.3",
156
- "@geekmidas/rate-limit": "^4.0.1",
157
- "@geekmidas/schema": "^1.0.4",
158
- "@geekmidas/services": "^2.0.1",
159
- "@geekmidas/testkit": "^3.1.1"
159
+ "@geekmidas/cache": "^9.1.0",
160
+ "@geekmidas/audit": "^9.1.0",
161
+ "@geekmidas/db": "^9.1.0",
162
+ "@geekmidas/envkit": "^9.1.0",
163
+ "@geekmidas/errors": "^9.1.0",
164
+ "@geekmidas/events": "^9.1.0",
165
+ "@geekmidas/rate-limit": "^9.1.0",
166
+ "@geekmidas/schema": "^9.1.0",
167
+ "@geekmidas/logger": "^9.1.0",
168
+ "@geekmidas/services": "^9.1.0",
169
+ "@geekmidas/testkit": "^9.1.0"
160
170
  },
161
171
  "peerDependencies": {
162
172
  "@middy/core": ">=6.3.1",
163
173
  "@types/aws-lambda": ">=8.10.92",
164
174
  "hono": ">=4.8.2",
165
175
  "msw": ">=2.0.0",
166
- "@geekmidas/audit": "^2.2.1",
167
- "@geekmidas/cache": "^1.1.2",
168
- "@geekmidas/db": "^1.1.1",
169
- "@geekmidas/envkit": "^1.1.1",
170
- "@geekmidas/errors": "^1.0.2",
171
- "@geekmidas/events": "^1.1.6",
172
- "@geekmidas/logger": "^1.0.3",
173
- "@geekmidas/rate-limit": "^4.0.1",
174
- "@geekmidas/schema": "^1.0.4",
175
- "@geekmidas/services": "^2.0.1"
176
+ "@geekmidas/audit": "^9.1.0",
177
+ "@geekmidas/cache": "^9.1.0",
178
+ "@geekmidas/db": "^9.1.0",
179
+ "@geekmidas/envkit": "^9.1.0",
180
+ "@geekmidas/errors": "^9.1.0",
181
+ "@geekmidas/events": "^9.1.0",
182
+ "@geekmidas/logger": "^9.1.0",
183
+ "@geekmidas/rate-limit": "^9.1.0",
184
+ "@geekmidas/schema": "^9.1.0",
185
+ "@geekmidas/services": "^9.1.0"
176
186
  },
177
187
  "peerDependenciesMeta": {
178
188
  "@geekmidas/audit": {
package/src/Construct.ts CHANGED
@@ -9,6 +9,7 @@ import type { Logger } from '@geekmidas/logger';
9
9
  import type { Service, ServiceContext } from '@geekmidas/services';
10
10
  import type { StandardSchemaV1 } from '@standard-schema/spec';
11
11
  import compact from 'lodash.compact';
12
+ import type { MonitorService } from './monitor';
12
13
 
13
14
  // Cache for service environment variables to handle singleton services
14
15
  // Stores both the full list and the optional subset so the same sniff result
@@ -60,6 +61,9 @@ export abstract class Construct<
60
61
  /** Database service for this construct (used by Endpoint for RLS/audit context) */
61
62
  public databaseService?: Service<TDatabaseServiceName, TDatabase>;
62
63
 
64
+ /** Monitors told about each run of this construct (see `.monitor()`). */
65
+ public monitors: MonitorService[] = [];
66
+
63
67
  constructor(
64
68
  public readonly type: ConstructType,
65
69
  public readonly logger: TLogger,
@@ -111,6 +115,7 @@ export abstract class Construct<
111
115
  this.publisherService,
112
116
  this.auditorStorageService,
113
117
  this.databaseService,
118
+ ...this.monitors,
114
119
  ]);
115
120
 
116
121
  try {
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Builder reuse: a configured base works, and nothing crosses between chains.
3
+ *
4
+ * Two properties, asserted for every field on every builder that carries state.
5
+ *
6
+ * **Persistence** — `const base = c.logger(log).timeout(60_000)` keeps its
7
+ * configuration for every construct built from it. It used to survive exactly
8
+ * one `.handle()`: the reset block at the end put every field back to its
9
+ * default, so the second cron silently came back on the console logger and
10
+ * the default timeout.
11
+ *
12
+ * **Isolation** — two chains off one base get only what each declared. They
13
+ * used to be the same object, so a chain's function came out holding the other
14
+ * chain's services too.
15
+ *
16
+ * Table-driven because the two properties are the same question asked of every
17
+ * field, and the failure mode being guarded against is precisely a field nobody
18
+ * remembered to handle.
19
+ */
20
+
21
+ import type { AuditStorage } from '@geekmidas/audit';
22
+ import type { EventPublisher } from '@geekmidas/events';
23
+ import { ConsoleLogger } from '@geekmidas/logger/console';
24
+ import type { Service } from '@geekmidas/services';
25
+ import { describe, expect, it } from 'vitest';
26
+ import { z } from 'zod';
27
+ import { CronBuilder } from '../crons/CronBuilder';
28
+ import { EndpointBuilder } from '../endpoints/EndpointBuilder';
29
+ import { FunctionBuilder } from '../functions/FunctionBuilder';
30
+ import { QueueBuilder } from '../queue/QueueBuilder';
31
+ import { SubscriberBuilder } from '../subscribers/SubscriberBuilder';
32
+ import { TopicBuilder } from '../topic/TopicBuilder';
33
+
34
+ const serviceA = {
35
+ serviceName: 'a' as const,
36
+ async register() {
37
+ return {};
38
+ },
39
+ };
40
+ const serviceB = {
41
+ serviceName: 'b' as const,
42
+ async register() {
43
+ return {};
44
+ },
45
+ };
46
+
47
+ const loggerA = new ConsoleLogger({ which: 'a' });
48
+ const loggerB = new ConsoleLogger({ which: 'b' });
49
+
50
+ const schemaA = z.object({ a: z.string() });
51
+ const schemaB = z.object({ b: z.string() });
52
+
53
+ const publisherA = {
54
+ serviceName: 'pa' as const,
55
+ async register() {
56
+ return {} as EventPublisher<any>;
57
+ },
58
+ };
59
+ const publisherB = {
60
+ serviceName: 'pb' as const,
61
+ async register() {
62
+ return {} as EventPublisher<any>;
63
+ },
64
+ };
65
+
66
+ const auditA = {
67
+ serviceName: 'aa' as const,
68
+ async register() {
69
+ return {} as AuditStorage;
70
+ },
71
+ };
72
+ const auditB = {
73
+ serviceName: 'ab' as const,
74
+ async register() {
75
+ return {} as AuditStorage;
76
+ },
77
+ };
78
+
79
+ const dbA = {
80
+ serviceName: 'da' as const,
81
+ async register() {
82
+ return {};
83
+ },
84
+ };
85
+ const dbB = {
86
+ serviceName: 'db' as const,
87
+ async register() {
88
+ return {};
89
+ },
90
+ };
91
+
92
+ const topicA = new TopicBuilder()
93
+ .topic('alpha')
94
+ .events({ 'a.one': z.object({}) });
95
+ const topicB = new TopicBuilder()
96
+ .topic('beta')
97
+ .events({ 'b.one': z.object({}) });
98
+
99
+ /** One field: how to set it, and how to read it back off what was built. */
100
+ interface Field<B> {
101
+ name: string;
102
+ a: unknown;
103
+ b: unknown;
104
+ set: (builder: B, value: any) => B;
105
+ get: (built: any) => unknown;
106
+ }
107
+
108
+ function reusable<B>(
109
+ label: string,
110
+ fresh: () => B,
111
+ build: (builder: B) => unknown,
112
+ fields: Field<B>[],
113
+ ) {
114
+ describe(label, () => {
115
+ for (const field of fields) {
116
+ it(`${field.name} — persists across builds off one base`, () => {
117
+ const base = field.set(fresh(), field.a);
118
+
119
+ // Built twice from the same base, with no clone in between: the
120
+ // terminal call must read the builder without resetting it.
121
+ expect(field.get(build(base))).toEqual(field.a);
122
+ expect(field.get(build(base))).toEqual(field.a);
123
+ });
124
+
125
+ it(`${field.name} — does not cross between chains off one base`, () => {
126
+ const base = fresh();
127
+ const one = field.set(base, field.a);
128
+ const two = field.set(base, field.b);
129
+
130
+ expect(field.get(build(one))).toEqual(field.a);
131
+ expect(field.get(build(two))).toEqual(field.b);
132
+ });
133
+
134
+ it(`${field.name} — leaves the base it was set on untouched`, () => {
135
+ const base = fresh();
136
+ const untouched = field.get(build(base));
137
+
138
+ field.set(base, field.a);
139
+
140
+ expect(field.get(build(base))).toEqual(untouched);
141
+ });
142
+ }
143
+ });
144
+ }
145
+
146
+ const names = (services: readonly Service[]) =>
147
+ services.map((s) => s.serviceName);
148
+
149
+ reusable(
150
+ 'FunctionBuilder',
151
+ () => new FunctionBuilder(),
152
+ (b) => b.handle(async () => null),
153
+ [
154
+ {
155
+ name: 'logger',
156
+ a: loggerA,
157
+ b: loggerB,
158
+ set: (b, v) => b.logger(v),
159
+ get: (f) => f.logger,
160
+ },
161
+ {
162
+ name: 'timeout',
163
+ a: 1000,
164
+ b: 2000,
165
+ set: (b, v) => b.timeout(v),
166
+ get: (f) => f.timeout,
167
+ },
168
+ {
169
+ name: 'memorySize',
170
+ a: 128,
171
+ b: 256,
172
+ set: (b, v) => b.memorySize(v),
173
+ get: (f) => f.memorySize,
174
+ },
175
+ {
176
+ name: 'services',
177
+ a: ['a'],
178
+ b: ['b'],
179
+ set: (b, v) => b.services([v[0] === 'a' ? serviceA : serviceB]),
180
+ get: (f) => names(f.services),
181
+ },
182
+ {
183
+ name: 'input',
184
+ a: schemaA,
185
+ b: schemaB,
186
+ set: (b, v) => b.input(v),
187
+ get: (f) => f.input,
188
+ },
189
+ {
190
+ name: 'output',
191
+ a: schemaA,
192
+ b: schemaB,
193
+ set: (b, v) => b.output(v),
194
+ get: (f) => f.outputSchema,
195
+ },
196
+ {
197
+ name: 'publisher',
198
+ a: publisherA,
199
+ b: publisherB,
200
+ set: (b, v) => b.publisher(v),
201
+ get: (f) => f.publisherService,
202
+ },
203
+ {
204
+ name: 'auditor',
205
+ a: auditA,
206
+ b: auditB,
207
+ set: (b, v) => b.auditor(v),
208
+ get: (f) => f.auditorStorageService,
209
+ },
210
+ {
211
+ name: 'database',
212
+ a: dbA,
213
+ b: dbB,
214
+ set: (b, v) => b.database(v),
215
+ get: (f) => f.databaseService,
216
+ },
217
+ // Accumulating, like services: each chain adds its own, so isolation
218
+ // means each holds only what it added — not that one holds more.
219
+ {
220
+ name: 'event',
221
+ a: ['ea'],
222
+ b: ['eb'],
223
+ set: (b, v) => b.event({ type: v[0] } as any),
224
+ get: (f) => f.events.map((e: any) => e.type),
225
+ },
226
+ ] as Field<any>[],
227
+ );
228
+
229
+ reusable(
230
+ 'CronBuilder',
231
+ () => new CronBuilder(),
232
+ (b) => b.handle(async () => null),
233
+ [
234
+ {
235
+ name: 'schedule',
236
+ a: 'rate(1 day)',
237
+ b: 'rate(2 days)',
238
+ set: (b, v) => b.schedule(v),
239
+ get: (c) => c.schedule,
240
+ },
241
+ {
242
+ name: 'logger',
243
+ a: loggerA,
244
+ b: loggerB,
245
+ set: (b, v) => b.logger(v),
246
+ get: (c) => c.logger,
247
+ },
248
+ {
249
+ name: 'timeout',
250
+ a: 1000,
251
+ b: 2000,
252
+ set: (b, v) => b.timeout(v),
253
+ get: (c) => c.timeout,
254
+ },
255
+ {
256
+ name: 'memorySize',
257
+ a: 128,
258
+ b: 256,
259
+ set: (b, v) => b.memorySize(v),
260
+ get: (c) => c.memorySize,
261
+ },
262
+ {
263
+ name: 'services',
264
+ a: ['a'],
265
+ b: ['b'],
266
+ set: (b, v) => b.services([v[0] === 'a' ? serviceA : serviceB]),
267
+ get: (c) => names(c.services),
268
+ },
269
+ {
270
+ name: 'input',
271
+ a: schemaA,
272
+ b: schemaB,
273
+ set: (b, v) => b.input(v),
274
+ get: (c) => c.input,
275
+ },
276
+ {
277
+ name: 'output',
278
+ a: schemaA,
279
+ b: schemaB,
280
+ set: (b, v) => b.output(v),
281
+ get: (c) => c.outputSchema,
282
+ },
283
+ {
284
+ name: 'publisher',
285
+ a: publisherA,
286
+ b: publisherB,
287
+ set: (b, v) => b.publisher(v),
288
+ get: (c) => c.publisherService,
289
+ },
290
+ {
291
+ name: 'database',
292
+ a: dbA,
293
+ b: dbB,
294
+ set: (b, v) => b.database(v),
295
+ get: (c) => c.databaseService,
296
+ },
297
+ {
298
+ name: 'event',
299
+ a: ['ea'],
300
+ b: ['eb'],
301
+ set: (b, v) => b.event({ type: v[0] } as any),
302
+ get: (c) => c.events.map((e: any) => e.type),
303
+ },
304
+ ] as Field<any>[],
305
+ );
306
+
307
+ reusable(
308
+ 'QueueBuilder',
309
+ () => new QueueBuilder().queue('base').message(z.object({})),
310
+ (b) => b.handle(async () => {}),
311
+ [
312
+ {
313
+ name: 'queue',
314
+ a: 'alpha',
315
+ b: 'beta',
316
+ set: (b, v) => b.queue(v),
317
+ get: (q) => q.name,
318
+ },
319
+ {
320
+ name: 'message',
321
+ a: schemaA,
322
+ b: schemaB,
323
+ set: (b, v) => b.message(v),
324
+ get: (q) => q.messageSchema,
325
+ },
326
+ {
327
+ name: 'timeout',
328
+ a: 1000,
329
+ b: 2000,
330
+ set: (b, v) => b.timeout(v),
331
+ get: (q) => q.timeout,
332
+ },
333
+ {
334
+ name: 'batchSize',
335
+ a: 5,
336
+ b: 10,
337
+ set: (b, v) => b.batchSize(v),
338
+ get: (q) => q.batchSize,
339
+ },
340
+ {
341
+ name: 'fifo',
342
+ a: true,
343
+ b: false,
344
+ set: (b, v) => b.fifo(v),
345
+ get: (q) => q.fifo,
346
+ },
347
+ {
348
+ name: 'logger',
349
+ a: loggerA,
350
+ b: loggerB,
351
+ set: (b, v) => b.logger(v),
352
+ get: (q) => q.logger,
353
+ },
354
+ {
355
+ name: 'services',
356
+ a: ['a'],
357
+ b: ['b'],
358
+ set: (b, v) => b.services([v[0] === 'a' ? serviceA : serviceB]),
359
+ get: (q) => names(q.services),
360
+ },
361
+ ] as Field<any>[],
362
+ );
363
+
364
+ reusable(
365
+ 'SubscriberBuilder',
366
+ () => new SubscriberBuilder(),
367
+ (b) => b.handle(async () => null),
368
+ [
369
+ {
370
+ name: 'topic',
371
+ a: 'alpha',
372
+ b: 'beta',
373
+ set: (b, v) => b.topic(v === 'alpha' ? topicA : topicB),
374
+ get: (s) => s.topicName,
375
+ },
376
+ {
377
+ name: 'timeout',
378
+ a: 1000,
379
+ b: 2000,
380
+ set: (b, v) => b.timeout(v),
381
+ get: (s) => s.timeout,
382
+ },
383
+ {
384
+ name: 'output',
385
+ a: schemaA,
386
+ b: schemaB,
387
+ set: (b, v) => b.output(v),
388
+ get: (s) => s.outputSchema,
389
+ },
390
+ {
391
+ name: 'logger',
392
+ a: loggerA,
393
+ b: loggerB,
394
+ set: (b, v) => b.logger(v),
395
+ get: (s) => s.logger,
396
+ },
397
+ {
398
+ name: 'publisher',
399
+ a: publisherA,
400
+ b: publisherB,
401
+ set: (b, v) => b.publisher(v),
402
+ get: (s) => s.publisherService,
403
+ },
404
+ {
405
+ name: 'services',
406
+ a: ['a'],
407
+ b: ['b'],
408
+ set: (b, v) => b.services([v[0] === 'a' ? serviceA : serviceB]),
409
+ get: (s) => names(s.services),
410
+ },
411
+ {
412
+ name: 'subscribe',
413
+ a: ['ea'],
414
+ b: ['eb'],
415
+ set: (b, v) => b.subscribe(v[0]),
416
+ get: (s) => s.subscribedEvents,
417
+ },
418
+ ] as Field<any>[],
419
+ );
420
+
421
+ reusable(
422
+ 'TopicBuilder',
423
+ // `.events()` is the terminal call and needs a name, so the base carries one.
424
+ () => new TopicBuilder().topic('base'),
425
+ (b) => b.events({ 'a.one': z.object({}) }),
426
+ [
427
+ {
428
+ name: 'topic',
429
+ a: 'alpha',
430
+ b: 'beta',
431
+ set: (b, v) => b.topic(v),
432
+ get: (t) => t.name,
433
+ },
434
+ {
435
+ name: 'logger',
436
+ a: loggerA,
437
+ b: loggerB,
438
+ set: (b, v) => b.logger(v),
439
+ get: (t) => t.logger,
440
+ },
441
+ ] as Field<any>[],
442
+ );
443
+
444
+ // Endpoints are usually built fresh per route by `EndpointFactory`, but a
445
+ // builder can still be shared — `const base = e.post('/x').services([db])` —
446
+ // and `body`/`query`/`params` used to write into one `schemas` object that
447
+ // every clone of the base pointed at.
448
+ reusable(
449
+ 'EndpointBuilder',
450
+ () => new EndpointBuilder('/users', 'POST'),
451
+ (b) => b.handle(async () => ({})),
452
+ [
453
+ {
454
+ name: 'description',
455
+ a: 'one',
456
+ b: 'two',
457
+ set: (b, v) => b.description(v),
458
+ get: (e) => e.description,
459
+ },
460
+ {
461
+ name: 'status',
462
+ a: 201,
463
+ b: 202,
464
+ set: (b, v) => b.status(v),
465
+ get: (e) => e.status,
466
+ },
467
+ {
468
+ name: 'tags',
469
+ a: ['a'],
470
+ b: ['b'],
471
+ set: (b, v) => b.tags(v),
472
+ get: (e) => e.tags,
473
+ },
474
+ {
475
+ name: 'memorySize',
476
+ a: 128,
477
+ b: 256,
478
+ set: (b, v) => b.memorySize(v),
479
+ get: (e) => e.memorySize,
480
+ },
481
+ {
482
+ name: 'timeout',
483
+ a: 1000,
484
+ b: 2000,
485
+ set: (b, v) => b.timeout(v),
486
+ get: (e) => e.timeout,
487
+ },
488
+ {
489
+ name: 'responseType',
490
+ a: 'text/plain',
491
+ b: 'text/csv',
492
+ set: (b, v) => b.responseType(v),
493
+ get: (e) => e.responseType,
494
+ },
495
+ {
496
+ name: 'body',
497
+ a: schemaA,
498
+ b: schemaB,
499
+ set: (b, v) => b.body(v),
500
+ get: (e) => e.input?.body,
501
+ },
502
+ {
503
+ name: 'query',
504
+ a: schemaA,
505
+ b: schemaB,
506
+ set: (b, v) => b.query(v),
507
+ get: (e) => e.input?.query,
508
+ },
509
+ {
510
+ name: 'params',
511
+ a: schemaA,
512
+ b: schemaB,
513
+ set: (b, v) => b.params(v),
514
+ get: (e) => e.input?.params,
515
+ },
516
+ {
517
+ name: 'output',
518
+ a: schemaA,
519
+ b: schemaB,
520
+ set: (b, v) => b.output(v),
521
+ get: (e) => e.outputSchema,
522
+ },
523
+ {
524
+ name: 'logger',
525
+ a: loggerA,
526
+ b: loggerB,
527
+ set: (b, v) => b.logger(v),
528
+ get: (e) => e.logger,
529
+ },
530
+ {
531
+ name: 'services',
532
+ a: ['a'],
533
+ b: ['b'],
534
+ set: (b, v) => b.services([v[0] === 'a' ? serviceA : serviceB]),
535
+ get: (e) => names(e.services),
536
+ },
537
+ {
538
+ name: 'publisher',
539
+ a: publisherA,
540
+ b: publisherB,
541
+ set: (b, v) => b.publisher(v),
542
+ get: (e) => e.publisherService,
543
+ },
544
+ {
545
+ name: 'event',
546
+ a: ['ea'],
547
+ b: ['eb'],
548
+ set: (b, v) => b.event({ type: v[0] } as any),
549
+ get: (e) => e.events.map((ev: any) => ev.type),
550
+ },
551
+ {
552
+ name: 'auditor',
553
+ a: auditA,
554
+ b: auditB,
555
+ set: (b, v) => b.auditor(v),
556
+ get: (e) => e.auditorStorageService,
557
+ },
558
+ {
559
+ name: 'audit',
560
+ a: [{ type: 'a' }],
561
+ b: [{ type: 'b' }],
562
+ set: (b, v) => b.audit(v),
563
+ get: (e) => e.audits,
564
+ },
565
+ {
566
+ name: 'database',
567
+ a: dbA,
568
+ b: dbB,
569
+ set: (b, v) => b.database(v),
570
+ get: (e) => e.databaseService,
571
+ },
572
+ {
573
+ name: 'rateLimit',
574
+ a: { limit: 1, windowMs: 1000 },
575
+ b: { limit: 2, windowMs: 1000 },
576
+ set: (b, v) => b.rateLimit(v),
577
+ get: (e) => e.rateLimit,
578
+ },
579
+ {
580
+ name: 'rls',
581
+ a: { bypass: true, config: undefined },
582
+ b: { bypass: false, config: { prefix: 'b' } },
583
+ set: (b, v) => b.rls(v.bypass ? false : v.config),
584
+ get: (e) => ({ bypass: e.rlsBypass === true, config: e.rlsConfig }),
585
+ },
586
+ ] as Field<any>[],
587
+ );