@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.
- package/CHANGELOG.md +71 -0
- package/dist/{AWSScheduledFunction-CTIa90EH.d.cts → AWSScheduledFunction-BHHlWlfV.d.cts} +2 -2
- package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts.map → AWSScheduledFunction-BHHlWlfV.d.cts.map} +1 -1
- package/dist/{AWSScheduledFunction-ysaCsdWx.d.mts → AWSScheduledFunction-BzGCUbkk.d.mts} +2 -2
- package/dist/{AWSScheduledFunction-CTIa90EH.d.cts.map → AWSScheduledFunction-BzGCUbkk.d.mts.map} +1 -1
- package/dist/{AWSScheduledFunction-DbkDr8CA.cjs → AWSScheduledFunction-C42adp8A.cjs} +16 -5
- package/dist/AWSScheduledFunction-C42adp8A.cjs.map +1 -0
- package/dist/{AWSScheduledFunction-CufMMhGB.mjs → AWSScheduledFunction-KP6lqbsW.mjs} +15 -4
- package/dist/AWSScheduledFunction-KP6lqbsW.mjs.map +1 -0
- package/dist/{Construct-r0U2zBwW.mjs → Construct-BWF5HxaM.mjs} +5 -2
- package/dist/Construct-BWF5HxaM.mjs.map +1 -0
- package/dist/{Construct-_1wX7p_Y.d.cts → Construct-JlyQkBkv.d.cts} +39 -4
- package/dist/Construct-JlyQkBkv.d.cts.map +1 -0
- package/dist/{Construct-vXMZJgRp.cjs → Construct-TGIM_E4o.cjs} +5 -2
- package/dist/Construct-TGIM_E4o.cjs.map +1 -0
- package/dist/{Construct-DCMuYOox.d.mts → Construct-pxwC6fDD.d.mts} +39 -4
- package/dist/Construct-pxwC6fDD.d.mts.map +1 -0
- package/dist/{Endpoint-CLPSIl6S.d.mts → Endpoint-B0P_pEMr.d.mts} +3 -3
- package/dist/{Endpoint-CLPSIl6S.d.mts.map → Endpoint-B0P_pEMr.d.mts.map} +1 -1
- package/dist/{Endpoint-CAzNbDEt.d.cts → Endpoint-Cwjhc1Hl.d.cts} +2 -2
- package/dist/Endpoint-Cwjhc1Hl.d.cts.map +1 -0
- package/dist/{HonoEndpointAdaptor-DJDT3PMx.mjs → HonoEndpointAdaptor-BMVwx2cz.mjs} +18 -11
- package/dist/HonoEndpointAdaptor-BMVwx2cz.mjs.map +1 -0
- package/dist/{HonoEndpointAdaptor-CkJENANP.cjs → HonoEndpointAdaptor-DPDsDi-D.cjs} +19 -12
- package/dist/HonoEndpointAdaptor-DPDsDi-D.cjs.map +1 -0
- package/dist/{Queue-HBDf0fqz.d.mts → Queue-BL68oDJ-.d.mts} +2 -2
- package/dist/{Queue-DaxjwmOQ.d.cts.map → Queue-BL68oDJ-.d.mts.map} +1 -1
- package/dist/{Queue-DaxjwmOQ.d.cts → Queue-DxJ6btRE.d.cts} +2 -2
- package/dist/{Queue-HBDf0fqz.d.mts.map → Queue-DxJ6btRE.d.cts.map} +1 -1
- package/dist/{Subscriber-B69ngPwl.d.cts → Subscriber-BpVhcGhX.d.cts} +2 -2
- package/dist/{Subscriber-B69ngPwl.d.cts.map → Subscriber-BpVhcGhX.d.cts.map} +1 -1
- package/dist/{Subscriber-D8Xm-86W.d.mts → Subscriber-DF6xsBAJ.d.mts} +2 -2
- package/dist/{Subscriber-D8Xm-86W.d.mts.map → Subscriber-DF6xsBAJ.d.mts.map} +1 -1
- package/dist/{Topic-DT10AqHk.d.mts → Topic-DJXoDeMP.d.cts} +3 -3
- package/dist/{Topic-C5ZMfveW.d.cts.map → Topic-DJXoDeMP.d.cts.map} +1 -1
- package/dist/{Topic-C5ZMfveW.d.cts → Topic-DxWQBgMN.d.mts} +3 -3
- package/dist/{Topic-DT10AqHk.d.mts.map → Topic-DxWQBgMN.d.mts.map} +1 -1
- package/dist/adaptors/aws.cjs +14 -7
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +6 -6
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +8 -8
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +14 -7
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +7 -6
- package/dist/adaptors/hono.d.cts +3 -3
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts +4 -4
- package/dist/adaptors/hono.d.mts.map +1 -1
- package/dist/adaptors/hono.mjs +7 -6
- package/dist/adaptors/testing.cjs +161 -126
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +7 -5
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +8 -6
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +162 -127
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/clone-I_WIpiFf.cjs +45 -0
- package/dist/clone-I_WIpiFf.cjs.map +1 -0
- package/dist/clone-pdXxc0t3.mjs +39 -0
- package/dist/clone-pdXxc0t3.mjs.map +1 -0
- package/dist/crons/index.cjs +14 -32
- package/dist/crons/index.cjs.map +1 -1
- package/dist/crons/index.d.cts +7 -7
- package/dist/crons/index.d.cts.map +1 -1
- package/dist/crons/index.d.mts +3 -3
- package/dist/crons/index.d.mts.map +1 -1
- package/dist/crons/index.mjs +14 -32
- package/dist/crons/index.mjs.map +1 -1
- package/dist/endpoints/index.cjs +74 -61
- package/dist/endpoints/index.cjs.map +1 -1
- package/dist/endpoints/index.d.cts +14 -6
- package/dist/endpoints/index.d.cts.map +1 -1
- package/dist/endpoints/index.d.mts +13 -5
- package/dist/endpoints/index.d.mts.map +1 -1
- package/dist/endpoints/index.mjs +74 -61
- package/dist/endpoints/index.mjs.map +1 -1
- package/dist/functions/index.cjs +3 -2
- package/dist/functions/index.d.cts +2 -2
- package/dist/functions/index.d.mts +2 -2
- package/dist/functions/index.mjs +3 -2
- package/dist/{functions-B-qt91O7.mjs → functions-EqTloHjo.mjs} +107 -46
- package/dist/functions-EqTloHjo.mjs.map +1 -0
- package/dist/{functions-C8i1kCMM.cjs → functions-SqtCDc9c.cjs} +118 -45
- package/dist/functions-SqtCDc9c.cjs.map +1 -0
- package/dist/{index-DP6fqv0I.d.mts → index-C7iynzLI.d.mts} +8 -2
- package/dist/index-C7iynzLI.d.mts.map +1 -0
- package/dist/{index-BGeTScmi.d.cts → index-DdgJRm4q.d.cts} +12 -6
- package/dist/index-DdgJRm4q.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/{lazyAccessors-DfqZR6C4.mjs → lazyAccessors-CCTK19xj.mjs} +3 -3
- package/dist/{lazyAccessors-DfqZR6C4.mjs.map → lazyAccessors-CCTK19xj.mjs.map} +1 -1
- package/dist/{lazyAccessors-DmTG8sQH.cjs → lazyAccessors-D04oftGw.cjs} +3 -3
- package/dist/{lazyAccessors-DmTG8sQH.cjs.map → lazyAccessors-D04oftGw.cjs.map} +1 -1
- package/dist/monitors/index.cjs +49 -0
- package/dist/monitors/index.cjs.map +1 -0
- package/dist/monitors/index.d.cts +45 -0
- package/dist/monitors/index.d.cts.map +1 -0
- package/dist/monitors/index.d.mts +45 -0
- package/dist/monitors/index.d.mts.map +1 -0
- package/dist/monitors/index.mjs +48 -0
- package/dist/monitors/index.mjs.map +1 -0
- package/dist/{processAudits-CKjjivyz.cjs → processAudits-BJZT5gmy.cjs} +2 -2
- package/dist/{processAudits-CKjjivyz.cjs.map → processAudits-BJZT5gmy.cjs.map} +1 -1
- package/dist/{processAudits-ySnizdMG.mjs → processAudits-CwyaaSQc.mjs} +1 -1
- package/dist/{processAudits-ySnizdMG.mjs.map → processAudits-CwyaaSQc.mjs.map} +1 -1
- package/dist/{publisher-lFQleddL.cjs → publisher-BZgcrVPy.cjs} +1 -1
- package/dist/{publisher-lFQleddL.cjs.map → publisher-BZgcrVPy.cjs.map} +1 -1
- package/dist/{publisher-BrFOF847.mjs → publisher-CnpeMv-r.mjs} +1 -1
- package/dist/{publisher-BrFOF847.mjs.map → publisher-CnpeMv-r.mjs.map} +1 -1
- package/dist/queue/index.cjs +9 -22
- package/dist/queue/index.cjs.map +1 -1
- package/dist/queue/index.d.cts +4 -4
- package/dist/queue/index.d.cts.map +1 -1
- package/dist/queue/index.d.mts +2 -2
- package/dist/queue/index.d.mts.map +1 -1
- package/dist/queue/index.mjs +9 -22
- package/dist/queue/index.mjs.map +1 -1
- package/dist/subscribers/index.cjs +9 -22
- package/dist/subscribers/index.cjs.map +1 -1
- package/dist/subscribers/index.d.cts +5 -5
- package/dist/subscribers/index.d.cts.map +1 -1
- package/dist/subscribers/index.d.mts +3 -3
- package/dist/subscribers/index.d.mts.map +1 -1
- package/dist/subscribers/index.mjs +9 -22
- package/dist/subscribers/index.mjs.map +1 -1
- package/dist/{telemetry-3vdxGn4q.d.mts → telemetry-C--EgYVr.d.mts} +1 -1
- package/dist/{telemetry-3vdxGn4q.d.mts.map → telemetry-C--EgYVr.d.mts.map} +1 -1
- package/dist/topic/index.cjs +4 -7
- package/dist/topic/index.cjs.map +1 -1
- package/dist/topic/index.d.cts +2 -2
- package/dist/topic/index.d.cts.map +1 -1
- package/dist/topic/index.d.mts +2 -2
- package/dist/topic/index.d.mts.map +1 -1
- package/dist/topic/index.mjs +4 -7
- package/dist/topic/index.mjs.map +1 -1
- package/dist/{types-BDNGr7pv.d.mts → types-DNngH0fX.d.mts} +1 -1
- package/dist/{types-BDNGr7pv.d.mts.map → types-DNngH0fX.d.mts.map} +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +32 -22
- package/src/Construct.ts +5 -0
- package/src/__tests__/builder-reuse.spec.ts +587 -0
- package/src/__tests__/monitor.spec.ts +636 -0
- package/src/clone.ts +38 -0
- package/src/crons/CronBuilder.ts +35 -45
- package/src/crons/__tests__/Cron.spec.ts +10 -2
- package/src/crons/__tests__/CronBuilder.state-isolation.spec.ts +64 -18
- package/src/endpoints/AmazonApiGatewayEndpointAdaptor.ts +23 -2
- package/src/endpoints/EndpointBuilder.ts +61 -70
- package/src/endpoints/EndpointFactory.ts +31 -0
- package/src/endpoints/HonoEndpointAdaptor.ts +16 -1
- package/src/endpoints/TestEndpointAdaptor.ts +194 -150
- package/src/endpoints/__tests__/EndpointBuilder.audit.spec.ts +6 -3
- package/src/endpoints/__tests__/EndpointBuilder.responseType.spec.ts +4 -1
- package/src/endpoints/__tests__/EndpointBuilder.spec.ts +16 -8
- package/src/functions/AWSLambdaFunction.ts +28 -2
- package/src/functions/BaseFunctionBuilder.ts +33 -22
- package/src/functions/FunctionBuilder.ts +41 -51
- package/src/functions/TestFunctionAdaptor.ts +66 -31
- package/src/functions/__tests__/FunctionBuilder.state-isolation.spec.ts +55 -12
- package/src/index.ts +6 -0
- package/src/monitor.ts +145 -0
- package/src/monitors/heartbeat.ts +86 -0
- package/src/monitors/index.ts +6 -0
- package/src/queue/QueueBuilder.ts +22 -22
- package/src/subscribers/SubscriberBuilder.ts +19 -22
- package/src/topic/TopicBuilder.ts +9 -7
- package/tsdown.config.ts +1 -0
- package/dist/AWSScheduledFunction-CufMMhGB.mjs.map +0 -1
- package/dist/AWSScheduledFunction-DbkDr8CA.cjs.map +0 -1
- package/dist/Construct-DCMuYOox.d.mts.map +0 -1
- package/dist/Construct-_1wX7p_Y.d.cts.map +0 -1
- package/dist/Construct-r0U2zBwW.mjs.map +0 -1
- package/dist/Construct-vXMZJgRp.cjs.map +0 -1
- package/dist/Endpoint-CAzNbDEt.d.cts.map +0 -1
- package/dist/HonoEndpointAdaptor-CkJENANP.cjs.map +0 -1
- package/dist/HonoEndpointAdaptor-DJDT3PMx.mjs.map +0 -1
- package/dist/functions-B-qt91O7.mjs.map +0 -1
- package/dist/functions-C8i1kCMM.cjs.map +0 -1
- package/dist/index-BGeTScmi.d.cts.map +0 -1
- 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
|
-
|
|
32
|
-
|
|
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(
|
|
50
|
-
expect((builder as any)._status).
|
|
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(
|
|
388
|
-
|
|
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
|
-
|
|
451
|
-
expect(
|
|
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(
|
|
342
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
50
|
-
return this;
|
|
47
|
+
return cloneWith(this, { _timeout: timeout });
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
memorySize(memorySize: number): this {
|
|
54
|
-
this
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
services,
|
|
163
|
+
const monitor = await startMonitors(
|
|
164
|
+
this.fn,
|
|
165
|
+
this.serviceDiscovery,
|
|
166
166
|
logger,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
222
|
+
// Register publisher service if provided in context
|
|
187
223
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
224
|
+
await publishEvents(
|
|
225
|
+
logger,
|
|
226
|
+
this.serviceDiscovery,
|
|
227
|
+
this.fn.events,
|
|
228
|
+
output,
|
|
229
|
+
this.fn.publisherService,
|
|
230
|
+
);
|
|
195
231
|
|
|
196
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
130
|
-
expect(builder1).toBe(
|
|
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
|
-
//
|
|
137
|
-
expect((
|
|
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