@geekmidas/constructs 9.0.2 → 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 +41 -0
  2. package/dist/{AWSScheduledFunction-Bn99MceY.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-Bn99MceY.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-DgfRslct.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-BVs9DvIj.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-DgfRslct.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-BVs9DvIj.d.cts.map +0 -1
  186. package/dist/index-DP6fqv0I.d.mts.map +0 -1
@@ -28,8 +28,10 @@ describe('EndpointBuilder', () => {
28
28
  const builder = new EndpointBuilder('/users', 'GET');
29
29
  const result = builder.description('Get all users');
30
30
 
31
- expect(result).toBe(builder); // Should return this for chaining
32
- expect((builder as any)._description).toBe('Get all users');
31
+ // The value lands on the returned builder, not on the one it came
32
+ // from — that is what keeps a configured base reusable.
33
+ expect((result as any)._description).toBe('Get all users');
34
+ expect((builder as any)._description).toBeUndefined();
33
35
  });
34
36
 
35
37
  it('should pass description to endpoint', () => {
@@ -46,8 +48,8 @@ describe('EndpointBuilder', () => {
46
48
  const builder = new EndpointBuilder('/users', 'POST');
47
49
  const result = builder.status(201);
48
50
 
49
- expect(result).toBe(builder); // Should return this for chaining
50
- expect((builder as any)._status).toBe(201);
51
+ expect((result as any)._status).toBe(201);
52
+ expect((builder as any)._status).toBeUndefined();
51
53
  });
52
54
 
53
55
  it('should pass status to endpoint', () => {
@@ -384,8 +386,10 @@ describe('EndpointBuilder', () => {
384
386
  const builder = new EndpointBuilder('/test', 'GET');
385
387
  const result = builder.authorize(authFn);
386
388
 
387
- expect(result).toBe(builder);
388
- expect((builder as any)._authorize).toBe(authFn);
389
+ expect((result as any)._authorize).toBe(authFn);
390
+ // The base keeps whatever it had — a fresh builder carries a default
391
+ // authorize, so the check is that it did not take the new one.
392
+ expect((builder as any)._authorize).not.toBe(authFn);
389
393
  });
390
394
 
391
395
  it('should pass .authorize() function to endpoint', () => {
@@ -447,8 +451,12 @@ describe('EndpointBuilder', () => {
447
451
  const builder2 = builder1.description('Test');
448
452
  const builder3 = builder2.status(200);
449
453
 
450
- expect(builder1).toBe(builder2);
451
- expect(builder2).toBe(builder3);
454
+ // Each step is a new builder that accumulates what came before.
455
+ expect(builder3).toBeInstanceOf(EndpointBuilder);
456
+ expect((builder3 as any)._description).toBe('Test');
457
+ expect((builder3 as any)._status).toBe(200);
458
+ expect(builder2).not.toBe(builder1);
459
+ expect(builder3).not.toBe(builder2);
452
460
  });
453
461
  });
454
462
 
@@ -13,6 +13,7 @@ import { runWithRequestContext } from '@geekmidas/services';
13
13
  import middy, { type MiddlewareObj } from '@middy/core';
14
14
  import type { StandardSchemaV1 } from '@standard-schema/spec';
15
15
  import type { Context, Handler } from 'aws-lambda';
16
+ import { startMonitors } from '../monitor';
16
17
  import type { Function, FunctionHandler } from './Function';
17
18
  import { FunctionBuilder } from './FunctionBuilder';
18
19
  import { FunctionExecutionWrapper } from './FunctionExecutionWrapper';
@@ -127,6 +128,10 @@ export class AWSLambdaFunction<
127
128
  onError: (req) => {
128
129
  const logger = req.event?.logger || this.fn.logger;
129
130
  logger.error(req.error || {}, 'Error processing function');
131
+ // Kept for the monitors, which otherwise only see the wrapped error.
132
+ if (req.event) {
133
+ (req.event as any).__error = req.error;
134
+ }
130
135
 
131
136
  // Re-throw the wrapped error to let Lambda handle it
132
137
  throw wrapError(req.error);
@@ -338,8 +343,29 @@ export class AWSLambdaFunction<
338
343
  const requestId = context.awsRequestId;
339
344
  const logger = this.fn.logger.child({ requestId }) as TLogger;
340
345
 
341
- return runWithRequestContext({ logger, requestId, startTime }, () =>
342
- chain(event as Parameters<typeof chain>[0], context),
346
+ return runWithRequestContext(
347
+ { logger, requestId, startTime },
348
+ async () => {
349
+ // Around the whole chain, not as a middleware: a middleware's
350
+ // `after` runs before audits flush and events publish, and its
351
+ // `onError` never runs once `error()` rethrows.
352
+ const monitor = await startMonitors(
353
+ this.fn,
354
+ this.serviceDiscovery,
355
+ logger,
356
+ );
357
+ try {
358
+ const output = await chain(
359
+ event as Parameters<typeof chain>[0],
360
+ context,
361
+ );
362
+ await monitor.success(output);
363
+ return output;
364
+ } catch (error) {
365
+ await monitor.failure((event as any)?.__error ?? error);
366
+ throw error;
367
+ }
368
+ },
343
369
  );
344
370
  };
345
371
 
@@ -10,6 +10,8 @@ import type { Service } from '@geekmidas/services';
10
10
  import type { StandardSchemaV1 } from '@standard-schema/spec';
11
11
  import get from 'lodash.get';
12
12
  import { ConstructType } from '../Construct';
13
+ import { cloneWith } from '../clone';
14
+ import { addMonitor, type MonitorService } from '../monitor';
13
15
 
14
16
  const DEFAULT_LOGGER = new ConsoleLogger() as any;
15
17
 
@@ -36,6 +38,7 @@ export abstract class BaseFunctionBuilder<
36
38
  protected _publisher?: Service<TEventPublisherServiceName, TEventPublisher>;
37
39
  protected _auditorStorage?: Service<TAuditStorageServiceName, TAuditStorage>;
38
40
  protected _databaseService?: Service<TDatabaseServiceName, TDatabase>;
41
+ public _monitors: MonitorService[] = [];
39
42
 
40
43
  static isStandardSchemaV1(s: unknown): s is StandardSchemaV1 {
41
44
  const schema = (s as StandardSchemaV1)['~standard'];
@@ -81,19 +84,27 @@ export abstract class BaseFunctionBuilder<
81
84
  abstract logger<T extends Logger>(logger: T): any;
82
85
 
83
86
  timeout(timeout: number): this {
84
- this._timeout = timeout;
85
- return this;
87
+ return cloneWith(this, { _timeout: timeout });
86
88
  }
87
89
 
88
90
  abstract output<T extends StandardSchemaV1>(schema: T): any;
89
91
 
90
92
  abstract input<T extends ComposableStandardSchema>(schema: T): any;
91
93
 
94
+ /**
95
+ * Report each run to a monitor — a heartbeat, an uptime check, an error
96
+ * tracker. Can be called more than once; every monitor is told.
97
+ */
98
+ monitor(monitor: MonitorService): this {
99
+ return cloneWith(this, { _monitors: addMonitor(this._monitors, monitor) });
100
+ }
101
+
92
102
  event<TEvent extends MappedEvent<TEventPublisher, OutSchema>>(
93
103
  event: TEvent,
94
104
  ): this {
95
- this._events.push(event);
96
- return this;
105
+ // Replaced rather than pushed: a clone shares whatever array the field
106
+ // points at, so an in-place push would be seen by the base it came from.
107
+ return cloneWith(this, { _events: [...this._events, event] });
97
108
  }
98
109
 
99
110
  publisher<T extends EventPublisher<any>, TName extends string>(
@@ -110,12 +121,12 @@ export abstract class BaseFunctionBuilder<
110
121
  TDatabase,
111
122
  TDatabaseServiceName
112
123
  > {
113
- this._publisher = publisher as unknown as Service<
114
- TEventPublisherServiceName,
115
- TEventPublisher
116
- >;
117
-
118
- return this as unknown as BaseFunctionBuilder<
124
+ return cloneWith(this, {
125
+ _publisher: publisher as unknown as Service<
126
+ TEventPublisherServiceName,
127
+ TEventPublisher
128
+ >,
129
+ }) as unknown as BaseFunctionBuilder<
119
130
  TInput,
120
131
  OutSchema,
121
132
  TServices,
@@ -143,12 +154,12 @@ export abstract class BaseFunctionBuilder<
143
154
  TDatabase,
144
155
  TDatabaseServiceName
145
156
  > {
146
- this._auditorStorage = storage as unknown as Service<
147
- TAuditStorageServiceName,
148
- TAuditStorage
149
- >;
150
-
151
- return this as unknown as BaseFunctionBuilder<
157
+ return cloneWith(this, {
158
+ _auditorStorage: storage as unknown as Service<
159
+ TAuditStorageServiceName,
160
+ TAuditStorage
161
+ >,
162
+ }) as unknown as BaseFunctionBuilder<
152
163
  TInput,
153
164
  OutSchema,
154
165
  TServices,
@@ -180,12 +191,12 @@ export abstract class BaseFunctionBuilder<
180
191
  T,
181
192
  TName
182
193
  > {
183
- this._databaseService = service as unknown as Service<
184
- TDatabaseServiceName,
185
- TDatabase
186
- >;
187
-
188
- return this as unknown as BaseFunctionBuilder<
194
+ return cloneWith(this, {
195
+ _databaseService: service as unknown as Service<
196
+ TDatabaseServiceName,
197
+ TDatabase
198
+ >,
199
+ }) as unknown as BaseFunctionBuilder<
189
200
  TInput,
190
201
  OutSchema,
191
202
  TServices,
@@ -1,17 +1,15 @@
1
1
  import type { AuditableAction, AuditStorage } from '@geekmidas/audit';
2
2
  import type { EventPublisher } from '@geekmidas/events';
3
3
  import type { Logger } from '@geekmidas/logger';
4
- import { ConsoleLogger } from '@geekmidas/logger/console';
5
4
  import type { ComposableStandardSchema } from '@geekmidas/schema';
6
5
  import type { Service } from '@geekmidas/services';
7
6
  import type { StandardSchemaV1 } from '@standard-schema/spec';
8
7
  import uniqBy from 'lodash.uniqby';
9
8
  import { ConstructType } from '../Construct';
9
+ import { cloneWith } from '../clone';
10
10
  import { BaseFunctionBuilder } from './BaseFunctionBuilder';
11
11
  import { Function, type FunctionHandler } from './Function';
12
12
 
13
- const DEFAULT_LOGGER = new ConsoleLogger() as any;
14
-
15
13
  export class FunctionBuilder<
16
14
  TInput extends ComposableStandardSchema,
17
15
  OutSchema extends StandardSchemaV1 | undefined = undefined,
@@ -46,13 +44,11 @@ export class FunctionBuilder<
46
44
  }
47
45
 
48
46
  override timeout(timeout: number): this {
49
- this._timeout = timeout;
50
- return this;
47
+ return cloneWith(this, { _timeout: timeout });
51
48
  }
52
49
 
53
50
  memorySize(memorySize: number): this {
54
- this._memorySize = memorySize;
55
- return this;
51
+ return cloneWith(this, { _memorySize: memorySize });
56
52
  }
57
53
 
58
54
  output<T extends StandardSchemaV1>(
@@ -70,9 +66,9 @@ export class FunctionBuilder<
70
66
  TDatabaseServiceName,
71
67
  TAuditAction
72
68
  > {
73
- this.outputSchema = schema as unknown as OutSchema;
74
-
75
- return this as unknown as FunctionBuilder<
69
+ return cloneWith(this, {
70
+ outputSchema: schema as unknown as OutSchema,
71
+ }) as unknown as FunctionBuilder<
76
72
  TInput,
77
73
  T,
78
74
  TServices,
@@ -102,9 +98,9 @@ export class FunctionBuilder<
102
98
  TDatabaseServiceName,
103
99
  TAuditAction
104
100
  > {
105
- this.inputSchema = schema as unknown as TInput;
106
-
107
- return this as unknown as FunctionBuilder<
101
+ return cloneWith(this, {
102
+ inputSchema: schema as unknown as TInput,
103
+ }) as unknown as FunctionBuilder<
108
104
  T,
109
105
  OutSchema,
110
106
  TServices,
@@ -134,12 +130,12 @@ export class FunctionBuilder<
134
130
  TDatabaseServiceName,
135
131
  TAuditAction
136
132
  > {
137
- this._services = uniqBy(
138
- [...this._services, ...services],
139
- (s) => s.serviceName,
140
- ) as TServices;
141
-
142
- return this as unknown as FunctionBuilder<
133
+ return cloneWith(this, {
134
+ _services: uniqBy(
135
+ [...this._services, ...services],
136
+ (s) => s.serviceName,
137
+ ) as TServices,
138
+ }) as unknown as FunctionBuilder<
143
139
  TInput,
144
140
  OutSchema,
145
141
  [...TServices, ...T],
@@ -169,9 +165,9 @@ export class FunctionBuilder<
169
165
  TDatabaseServiceName,
170
166
  TAuditAction
171
167
  > {
172
- this._logger = logger as unknown as TLogger;
173
-
174
- return this as unknown as FunctionBuilder<
168
+ return cloneWith(this, {
169
+ _logger: logger as unknown as TLogger,
170
+ }) as unknown as FunctionBuilder<
175
171
  TInput,
176
172
  OutSchema,
177
173
  TServices,
@@ -201,12 +197,12 @@ export class FunctionBuilder<
201
197
  TDatabaseServiceName,
202
198
  TAuditAction
203
199
  > {
204
- this._publisher = publisher as unknown as Service<
205
- TEventPublisherServiceName,
206
- TEventPublisher
207
- >;
208
-
209
- return this as unknown as FunctionBuilder<
200
+ return cloneWith(this, {
201
+ _publisher: publisher as unknown as Service<
202
+ TEventPublisherServiceName,
203
+ TEventPublisher
204
+ >,
205
+ }) as unknown as FunctionBuilder<
210
206
  TInput,
211
207
  OutSchema,
212
208
  TServices,
@@ -236,12 +232,12 @@ export class FunctionBuilder<
236
232
  TDatabaseServiceName,
237
233
  TAuditAction
238
234
  > {
239
- this._auditorStorage = storage as unknown as Service<
240
- TAuditStorageServiceName,
241
- TAuditStorage
242
- >;
243
-
244
- return this as unknown as FunctionBuilder<
235
+ return cloneWith(this, {
236
+ _auditorStorage: storage as unknown as Service<
237
+ TAuditStorageServiceName,
238
+ TAuditStorage
239
+ >,
240
+ }) as unknown as FunctionBuilder<
245
241
  TInput,
246
242
  OutSchema,
247
243
  TServices,
@@ -307,12 +303,12 @@ export class FunctionBuilder<
307
303
  TName,
308
304
  TAuditAction
309
305
  > {
310
- this._databaseService = service as unknown as Service<
311
- TDatabaseServiceName,
312
- TDatabase
313
- >;
314
-
315
- return this as unknown as FunctionBuilder<
306
+ return cloneWith(this, {
307
+ _databaseService: service as unknown as Service<
308
+ TDatabaseServiceName,
309
+ TDatabase
310
+ >,
311
+ }) as unknown as FunctionBuilder<
316
312
  TInput,
317
313
  OutSchema,
318
314
  TServices,
@@ -374,17 +370,11 @@ export class FunctionBuilder<
374
370
  this._databaseService,
375
371
  );
376
372
 
377
- // Reset builder state after creating the function to prevent pollution
378
- this._services = [] as Service[] as TServices;
379
- this._logger = DEFAULT_LOGGER;
380
- this._events = [];
381
- this._publisher = undefined;
382
- this._auditorStorage = undefined;
383
- this._databaseService = undefined;
384
- this.inputSchema = undefined;
385
- this.outputSchema = undefined;
386
- this._timeout = undefined;
387
- this._memorySize = undefined;
373
+ // No reset. `.handle()` reads this builder and leaves it alone, so a
374
+ // configured base keeps its configuration for every construct built
375
+ // from it. The reset that used to be here wiped the base on first use.
376
+
377
+ func.monitors = this._monitors;
388
378
 
389
379
  return func;
390
380
  }
@@ -15,6 +15,7 @@ import {
15
15
  type ServiceRecord,
16
16
  } from '@geekmidas/services';
17
17
  import type { StandardSchemaV1 } from '@standard-schema/spec';
18
+ import { startMonitors } from '../monitor';
18
19
  import { publishEvents } from '../publisher';
19
20
  import type { Function } from './Function';
20
21
  import { FunctionBuilder } from './FunctionBuilder';
@@ -159,42 +160,76 @@ export class TestFunctionAdaptor<
159
160
  const startTime = Date.now();
160
161
 
161
162
  return runWithRequestContext({ logger, requestId, startTime }, async () => {
162
- // Execute the function
163
- const response = await this.fn.fn({
164
- input: parsedInput,
165
- services,
163
+ const monitor = await startMonitors(
164
+ this.fn,
165
+ this.serviceDiscovery,
166
166
  logger,
167
- db,
168
- auditor,
169
- } as any);
170
-
171
- // Parse output if schema is provided
172
- const output = await this.fn.parseOutput(response);
173
-
174
- // Flush audits if any were recorded
175
- if (auditor) {
176
- const records = auditor.getRecords();
177
- if (records.length > 0) {
178
- logger.debug(
179
- { auditCount: records.length },
180
- 'Flushing function audits',
181
- );
182
- await auditor.flush();
183
- }
167
+ );
168
+ try {
169
+ const output = await this.run({
170
+ parsedInput,
171
+ services,
172
+ logger,
173
+ db,
174
+ auditor,
175
+ });
176
+ await monitor.success(output);
177
+ return output;
178
+ } catch (error) {
179
+ await monitor.failure(error);
180
+ throw error;
181
+ }
182
+ }) as Promise<InferStandardSchema<TOutSchema>>;
183
+ }
184
+
185
+ private async run({
186
+ parsedInput,
187
+ services,
188
+ logger,
189
+ db,
190
+ auditor,
191
+ }: {
192
+ parsedInput: unknown;
193
+ services: ServiceRecord<TServices>;
194
+ logger: TLogger;
195
+ db: TDatabase | undefined;
196
+ auditor: Auditor<TAuditAction> | undefined;
197
+ }): Promise<InferStandardSchema<TOutSchema>> {
198
+ // Execute the function
199
+ const response = await this.fn.fn({
200
+ input: parsedInput,
201
+ services,
202
+ logger,
203
+ db,
204
+ auditor,
205
+ } as any);
206
+
207
+ // Parse output if schema is provided
208
+ const output = await this.fn.parseOutput(response);
209
+
210
+ // Flush audits if any were recorded
211
+ if (auditor) {
212
+ const records = auditor.getRecords();
213
+ if (records.length > 0) {
214
+ logger.debug(
215
+ { auditCount: records.length },
216
+ 'Flushing function audits',
217
+ );
218
+ await auditor.flush();
184
219
  }
220
+ }
185
221
 
186
- // Register publisher service if provided in context
222
+ // Register publisher service if provided in context
187
223
 
188
- await publishEvents(
189
- logger,
190
- this.serviceDiscovery,
191
- this.fn.events,
192
- output,
193
- this.fn.publisherService,
194
- );
224
+ await publishEvents(
225
+ logger,
226
+ this.serviceDiscovery,
227
+ this.fn.events,
228
+ output,
229
+ this.fn.publisherService,
230
+ );
195
231
 
196
- return output;
197
- }) as Promise<InferStandardSchema<TOutSchema>>;
232
+ return output;
198
233
  }
199
234
  }
200
235
 
@@ -31,6 +31,8 @@ const ServiceB = {
31
31
  },
32
32
  } satisfies Service<'b', any>;
33
33
 
34
+ const customLogger = new ConsoleLogger({ app: 'reusable-base' });
35
+
34
36
  describe('FunctionBuilder - State Isolation', () => {
35
37
  describe('singleton instance state reset', () => {
36
38
  it('should reset services after handle() is called', () => {
@@ -122,25 +124,26 @@ describe('FunctionBuilder - State Isolation', () => {
122
124
  it('should not share references between different builder chains', () => {
123
125
  const f = new FunctionBuilder();
124
126
 
125
- // Start two separate chains
127
+ // Two chains off one base. They used to be the *same object* — every
128
+ // method mutated the base and returned it — so chain 1's function came
129
+ // out holding chain 2's services as well, and the base only survived
130
+ // because `.handle()` wiped it afterwards. That wipe is also what made
131
+ // a configured base unusable twice.
126
132
  const builder1 = f.services([ServiceA]);
127
133
  const builder2 = f.services([ServiceB]);
128
134
 
129
- // They should be the same instance (singleton)
130
- expect(builder1).toBe(builder2);
131
- expect(builder1).toBe(f);
135
+ expect(builder1).not.toBe(builder2);
136
+ expect(builder1).not.toBe(f);
132
137
 
133
- // But after handle, state is reset
134
138
  const fn1 = builder1.handle(async () => ({}));
139
+ const fn2 = builder2.handle(async () => ({}));
135
140
 
136
- // Now builder2 should have reset state
137
- expect((builder2 as any)._services).toEqual([]);
138
-
139
- // Add services again
140
- const fn2 = builder2.services([ServiceB]).handle(async () => ({}));
141
-
142
- expect(fn1.services.map((s) => s.serviceName)).toEqual(['a', 'b']);
141
+ // Each function gets exactly what its own chain declared.
142
+ expect(fn1.services.map((s) => s.serviceName)).toEqual(['a']);
143
143
  expect(fn2.services.map((s) => s.serviceName)).toEqual(['b']);
144
+
145
+ // And the base is untouched, so it stays reusable.
146
+ expect((f as any)._services).toEqual([]);
144
147
  });
145
148
  });
146
149
 
@@ -204,4 +207,44 @@ describe('FunctionBuilder - State Isolation', () => {
204
207
  expect((fn2 as any).auditorStorageService).toBeUndefined();
205
208
  });
206
209
  });
210
+
211
+ describe('a configured base, reused', () => {
212
+ it('carries its configuration into every function built from it', () => {
213
+ const base = new FunctionBuilder().logger(customLogger).timeout(60_000);
214
+
215
+ const first = base.services([ServiceA]).handle(async () => ({}));
216
+ const second = base.services([ServiceB]).handle(async () => ({}));
217
+
218
+ // The point of the whole shape: `const base = f.logger(log)` is a base you
219
+ // can keep. It used to survive exactly one `.handle()` — the reset put
220
+ // the logger and the timeout back to their defaults — so the second
221
+ // function built from it silently logged somewhere else and timed out
222
+ // on a different clock.
223
+ for (const built of [first, second]) {
224
+ expect(built.logger).toBe(customLogger);
225
+ expect(built.timeout).toBe(60_000);
226
+ }
227
+
228
+ // And each still gets only what its own chain declared.
229
+ expect(first.services.map((s) => s.serviceName)).toEqual(['a']);
230
+ expect(second.services.map((s) => s.serviceName)).toEqual(['b']);
231
+ });
232
+
233
+ it('persists configuration when handle() is called on the base itself', () => {
234
+ const base = new FunctionBuilder().logger(customLogger).timeout(60_000);
235
+
236
+ // No clone in between: `.handle()` is called straight on the base,
237
+ // twice. Cloning the config methods was not enough on its own —
238
+ // `.handle()` still ran a reset block that wiped whatever it was
239
+ // called on, so the second function came back on the default logger and
240
+ // the default timeout.
241
+ const first = base.handle(async () => ({}));
242
+ const second = base.handle(async () => ({}));
243
+
244
+ for (const built of [first, second]) {
245
+ expect(built.logger).toBe(customLogger);
246
+ expect(built.timeout).toBe(60_000);
247
+ }
248
+ });
249
+ });
207
250
  });
package/src/index.ts CHANGED
@@ -8,6 +8,12 @@ export {
8
8
  snifferContext,
9
9
  sniffService,
10
10
  } from './Construct';
11
+ // Monitoring interface
12
+ export type {
13
+ ConstructMonitor,
14
+ MonitorContext,
15
+ MonitorService,
16
+ } from './monitor';
11
17
  // Telemetry interface
12
18
  export type {
13
19
  Telemetry,