From 8f9a030229390156bc9455e91cf7215266353fe3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 24 May 2025 18:00:10 +0200 Subject: [PATCH 1/5] fix(event-handler): fix decorated scope in appsync events --- .../appsync-events/AppSyncEventsResolver.ts | 14 +-- .../src/appsync-events/Router.ts | 97 ++++++++++++++++++- .../AppSyncEventsResolver.test.ts | 89 +++++++++++++++++ 3 files changed, 189 insertions(+), 11 deletions(-) diff --git a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts index 457ddbc30d..3e54d74529 100644 --- a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts +++ b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts @@ -114,11 +114,11 @@ class AppSyncEventsResolver extends Router { if (aggregate) { try { return { - events: await (handler as OnPublishHandlerAggregateFn).apply(this, [ + events: await (handler as OnPublishHandlerAggregateFn)( event.events, event, - context, - ]), + context + ), }; } catch (error) { this.logger.error(`An error occurred in handler ${path}`, error); @@ -131,11 +131,11 @@ class AppSyncEventsResolver extends Router { event.events.map(async (message) => { const { id, payload } = message; try { - const result = await (handler as OnPublishHandlerFn).apply(this, [ + const result = await (handler as OnPublishHandlerFn)( payload, event, - context, - ]); + context + ); return { id, payload: result, @@ -173,7 +173,7 @@ class AppSyncEventsResolver extends Router { } const { handler } = routeHandlerOptions; try { - await (handler as OnSubscribeHandler).apply(this, [event, context]); + await (handler as OnSubscribeHandler)(event, context); } catch (error) { this.logger.error(`An error occurred in handler ${path}`, error); if (error instanceof UnauthorizedException) throw error; diff --git a/packages/event-handler/src/appsync-events/Router.ts b/packages/event-handler/src/appsync-events/Router.ts index f9402b53c8..32a9c267bc 100644 --- a/packages/event-handler/src/appsync-events/Router.ts +++ b/packages/event-handler/src/appsync-events/Router.ts @@ -9,6 +9,9 @@ import type { } from '../types/appsync-events.js'; import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; +// Simple global approach - store the last instance per router +const routerInstanceMap = new WeakMap(); + /** * Class for registering routes for the `onPublish` and `onSubscribe` events in AWS AppSync Events APIs. */ @@ -194,11 +197,22 @@ class Router { return; } - return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + return (target, _propertyKey, descriptor: PropertyDescriptor) => { const routeOptions = isRecord(handler) ? handler : options; + const originalMethod = descriptor?.value; + const routerInstance = this; + + this.#bindResolveMethodScope(target); + + // Create a handler that uses the captured instance + const boundHandler = (...args: unknown[]) => { + const instance = routerInstanceMap.get(routerInstance); + return originalMethod?.apply(instance, args); + }; + this.onPublishRegistry.register({ path, - handler: descriptor.value, + handler: boundHandler, aggregate: (routeOptions?.aggregate ?? false) as T, }); return descriptor; @@ -273,14 +287,89 @@ class Router { return; } - return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + return (target, propertyKey, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor?.value; + const routerInstance = this; + + // Patch any method that might call resolve() to capture instance + this.#bindResolveMethodScope(target); + + // Create a handler that uses the captured instance + const boundHandler = (...args: unknown[]) => { + const instance = routerInstanceMap.get(routerInstance); + return originalMethod?.apply(instance, args); + }; + this.onSubscribeRegistry.register({ path, - handler: descriptor.value, + handler: boundHandler, }); return descriptor; }; } + + /** + * Binds the resolve method scope to the target object. + * + * We patch any method that might call `resolve()` to ensure that + * the class instance is captured correctly when the method is resolved. We need + * to do this because when a method is decorated, it loses its context and + * the `this` keyword inside the method no longer refers to the class instance of the decorated method. + * + * We need to apply this two-step process because the decorator is applied to the method + * before the class instance is created, so we cannot capture the instance directly. + * + * @param target - The target object whose methods will be patched to capture the instance scope + */ + #bindResolveMethodScope(target: object) { + const routerInstance = this; + + // Patch any method that might call resolve() to capture instance + if (!target.constructor.prototype._powertoolsPatched) { + target.constructor.prototype._powertoolsPatched = true; + + // Get all method names from the prototype + const proto = target.constructor.prototype; + const methodNames = Object.getOwnPropertyNames(proto); + + for (const methodName of methodNames) { + if (methodName === 'constructor') continue; + + const methodDescriptor = Object.getOwnPropertyDescriptor( + proto, + methodName + ); + if ( + methodDescriptor?.value && + typeof methodDescriptor.value === 'function' + ) { + const originalMethodRef = methodDescriptor.value; + const methodSource = originalMethodRef.toString(); + + // Check if this method calls .resolve() on our router instance + if ( + methodSource.includes('.resolve(') || + methodSource.includes('.resolve ') + ) { + const patchedMethod = function (this: unknown, ...args: unknown[]) { + // Capture instance when any method that calls resolve is called + if (this && typeof this === 'object') { + routerInstanceMap.set(routerInstance, this); + } + return originalMethodRef.apply(this, args); + }; + + Object.defineProperty(proto, methodName, { + value: patchedMethod, + writable: true, + configurable: true, + enumerable: true, + }); + } + } + } + } + } } export { Router }; diff --git a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index d75bede42f..0e153da91e 100644 --- a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -1,9 +1,11 @@ import context from '@aws-lambda-powertools/testing-utils/context'; +import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncEventsResolver, UnauthorizedException, } from '../../../src/appsync-events/index.js'; +import type { AppSyncEventsSubscribeEvent } from '../../../src/types/appsync-events.js'; import { onPublishEventFactory, onSubscribeEventFactory, @@ -63,6 +65,93 @@ describe('Class: AppSyncEventsResolver', () => { }); }); + it('preserves the scope when decorating methods', async () => { + // Prepare + const app = new AppSyncEventsResolver({ logger: console }); + + class Lambda { + public scope = 'scoped'; + + @app.onPublish('/foo') + public async handleFoo(payload: string) { + return `${this.scope} ${payload}`; + } + + public async handler(event: unknown, context: Context) { + return this.stuff(event, context); + } + + async stuff(event: unknown, context: Context) { + return app.resolve(event, context); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + const result = await handler( + onPublishEventFactory( + [ + { + id: '1', + payload: 'foo', + }, + ], + { + path: '/foo', + segments: ['foo'], + } + ), + context + ); + + // Assess + expect(result).toEqual({ + events: [ + { + id: '1', + payload: 'scoped foo', + }, + ], + }); + }); + + it('preserves the scope when decorating methods', async () => { + // Prepare + const app = new AppSyncEventsResolver({ logger: console }); + + class Lambda { + public scope = 'scoped'; + + @app.onSubscribe('/foo') + public async handleFoo(payload: AppSyncEventsSubscribeEvent) { + console.debug(`${this.scope} ${payload.info.channel.path}`); + } + + public async handler(event: unknown, context: Context) { + return this.stuff(event, context); + } + + async stuff(event: unknown, context: Context) { + return app.resolve(event, context); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + await handler( + onSubscribeEventFactory({ + path: '/foo', + segments: ['foo'], + }), + context + ); + + // Assess + expect(console.debug).toHaveBeenCalledWith('scoped /foo'); + }); + it('returns null if there are no onSubscribe handlers', async () => { // Prepare const app = new AppSyncEventsResolver({ logger: console }); From 84cf6c49f590bca6c6b34595a91192cdf65ab606 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 24 May 2025 18:04:09 +0200 Subject: [PATCH 2/5] chore: fix imports --- .../tests/unit/appsync-events/AppSyncEventsResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index 0e153da91e..9896b091b9 100644 --- a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -4,12 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncEventsResolver, UnauthorizedException, -} from '../../../src/appsync-events/index.js'; -import type { AppSyncEventsSubscribeEvent } from '../../../src/types/appsync-events.js'; +} from '../../src/appsync-events/index.js'; +import type { AppSyncEventsSubscribeEvent } from '../../src/types/appsync-events.js'; import { onPublishEventFactory, onSubscribeEventFactory, -} from '../../helpers/factories.js'; +} from '../helpers/factories.js'; describe('Class: AppSyncEventsResolver', () => { beforeEach(() => { From 15c47c806c055cdd9ed484639df5a77e80f850ef Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 26 May 2025 18:56:45 +0200 Subject: [PATCH 3/5] chore: fix imports --- .../tests/unit/appsync-events/AppSyncEventsResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index 9896b091b9..0e153da91e 100644 --- a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -4,12 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncEventsResolver, UnauthorizedException, -} from '../../src/appsync-events/index.js'; -import type { AppSyncEventsSubscribeEvent } from '../../src/types/appsync-events.js'; +} from '../../../src/appsync-events/index.js'; +import type { AppSyncEventsSubscribeEvent } from '../../../src/types/appsync-events.js'; import { onPublishEventFactory, onSubscribeEventFactory, -} from '../helpers/factories.js'; +} from '../../helpers/factories.js'; describe('Class: AppSyncEventsResolver', () => { beforeEach(() => { From df326ddd9d252bb5fad7a106db41ba22411aa54f Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 12 Jun 2025 10:15:27 +0200 Subject: [PATCH 4/5] fix: second iteration --- .../appsync-events/AppSyncEventsResolver.ts | 43 ++++--- .../src/appsync-events/Router.ts | 92 +-------------- .../event-handler/src/types/appsync-events.ts | 51 +++++++- packages/event-handler/src/types/index.ts | 2 + .../AppSyncEventsResolver.test.ts | 109 +++++++++++------- 5 files changed, 146 insertions(+), 151 deletions(-) diff --git a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts index 3e54d74529..efe473eb7e 100644 --- a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts +++ b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts @@ -5,6 +5,7 @@ import type { OnPublishHandlerAggregateFn, OnPublishHandlerFn, OnSubscribeHandler, + ResolveOptions, } from '../types/appsync-events.js'; import { Router } from './Router.js'; import { UnauthorizedException } from './errors.js'; @@ -67,7 +68,9 @@ class AppSyncEventsResolver extends Router { * } * * async handler(event, context) { - * return app.resolve(event, context); + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); * } * } * @@ -78,7 +81,11 @@ class AppSyncEventsResolver extends Router { * @param event - The incoming event from AppSync Events * @param context - The context object provided by AWS Lambda */ - public async resolve(event: unknown, context: Context) { + public async resolve( + event: unknown, + context: Context, + options?: ResolveOptions + ) { if (!isAppSyncEventsEvent(event)) { this.logger.warn( 'Received an event that is not compatible with this resolver' @@ -87,11 +94,12 @@ class AppSyncEventsResolver extends Router { } if (isAppSyncEventsPublishEvent(event)) { - return await this.handleOnPublish(event, context); + return await this.handleOnPublish(event, context, options); } return await this.handleOnSubscribe( event as AppSyncEventsSubscribeEvent, - context + context, + options ); } @@ -100,10 +108,12 @@ class AppSyncEventsResolver extends Router { * * @param event - The incoming event from AppSync Events * @param context - The context object provided by AWS Lambda + * @param options - Optional resolve options */ protected async handleOnPublish( event: AppSyncEventsPublishEvent, - context: Context + context: Context, + options?: ResolveOptions ) { const { path } = event.info.channel; const routeHandlerOptions = this.onPublishRegistry.resolve(path); @@ -114,10 +124,9 @@ class AppSyncEventsResolver extends Router { if (aggregate) { try { return { - events: await (handler as OnPublishHandlerAggregateFn)( - event.events, - event, - context + events: await (handler as OnPublishHandlerAggregateFn).apply( + options?.scope || this, + [event.events, event, context] ), }; } catch (error) { @@ -131,10 +140,9 @@ class AppSyncEventsResolver extends Router { event.events.map(async (message) => { const { id, payload } = message; try { - const result = await (handler as OnPublishHandlerFn)( - payload, - event, - context + const result = await (handler as OnPublishHandlerFn).apply( + options?.scope || this, + [payload, event, context] ); return { id, @@ -161,10 +169,12 @@ class AppSyncEventsResolver extends Router { * * @param event - The incoming event from AppSync Events * @param context - The context object provided by AWS Lambda + * @param options - Optional resolve options */ protected async handleOnSubscribe( event: AppSyncEventsSubscribeEvent, - context: Context + context: Context, + options?: ResolveOptions ) { const { path } = event.info.channel; const routeHandlerOptions = this.onSubscribeRegistry.resolve(path); @@ -173,7 +183,10 @@ class AppSyncEventsResolver extends Router { } const { handler } = routeHandlerOptions; try { - await (handler as OnSubscribeHandler)(event, context); + await (handler as OnSubscribeHandler).apply(options?.scope || this, [ + event, + context, + ]); } catch (error) { this.logger.error(`An error occurred in handler ${path}`, error); if (error instanceof UnauthorizedException) throw error; diff --git a/packages/event-handler/src/appsync-events/Router.ts b/packages/event-handler/src/appsync-events/Router.ts index 32a9c267bc..7326fc1319 100644 --- a/packages/event-handler/src/appsync-events/Router.ts +++ b/packages/event-handler/src/appsync-events/Router.ts @@ -199,20 +199,9 @@ class Router { return (target, _propertyKey, descriptor: PropertyDescriptor) => { const routeOptions = isRecord(handler) ? handler : options; - const originalMethod = descriptor?.value; - const routerInstance = this; - - this.#bindResolveMethodScope(target); - - // Create a handler that uses the captured instance - const boundHandler = (...args: unknown[]) => { - const instance = routerInstanceMap.get(routerInstance); - return originalMethod?.apply(instance, args); - }; - this.onPublishRegistry.register({ path, - handler: boundHandler, + handler: descriptor?.value, aggregate: (routeOptions?.aggregate ?? false) as T, }); return descriptor; @@ -287,89 +276,14 @@ class Router { return; } - return (target, propertyKey, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor?.value; - const routerInstance = this; - - // Patch any method that might call resolve() to capture instance - this.#bindResolveMethodScope(target); - - // Create a handler that uses the captured instance - const boundHandler = (...args: unknown[]) => { - const instance = routerInstanceMap.get(routerInstance); - return originalMethod?.apply(instance, args); - }; - + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { this.onSubscribeRegistry.register({ path, - handler: boundHandler, + handler: descriptor?.value, }); return descriptor; }; } - - /** - * Binds the resolve method scope to the target object. - * - * We patch any method that might call `resolve()` to ensure that - * the class instance is captured correctly when the method is resolved. We need - * to do this because when a method is decorated, it loses its context and - * the `this` keyword inside the method no longer refers to the class instance of the decorated method. - * - * We need to apply this two-step process because the decorator is applied to the method - * before the class instance is created, so we cannot capture the instance directly. - * - * @param target - The target object whose methods will be patched to capture the instance scope - */ - #bindResolveMethodScope(target: object) { - const routerInstance = this; - - // Patch any method that might call resolve() to capture instance - if (!target.constructor.prototype._powertoolsPatched) { - target.constructor.prototype._powertoolsPatched = true; - - // Get all method names from the prototype - const proto = target.constructor.prototype; - const methodNames = Object.getOwnPropertyNames(proto); - - for (const methodName of methodNames) { - if (methodName === 'constructor') continue; - - const methodDescriptor = Object.getOwnPropertyDescriptor( - proto, - methodName - ); - if ( - methodDescriptor?.value && - typeof methodDescriptor.value === 'function' - ) { - const originalMethodRef = methodDescriptor.value; - const methodSource = originalMethodRef.toString(); - - // Check if this method calls .resolve() on our router instance - if ( - methodSource.includes('.resolve(') || - methodSource.includes('.resolve ') - ) { - const patchedMethod = function (this: unknown, ...args: unknown[]) { - // Capture instance when any method that calls resolve is called - if (this && typeof this === 'object') { - routerInstanceMap.set(routerInstance, this); - } - return originalMethodRef.apply(this, args); - }; - - Object.defineProperty(proto, methodName, { - value: patchedMethod, - writable: true, - configurable: true, - enumerable: true, - }); - } - } - } - } - } } export { Router }; diff --git a/packages/event-handler/src/types/appsync-events.ts b/packages/event-handler/src/types/appsync-events.ts index 1367cacd93..a225446190 100644 --- a/packages/event-handler/src/types/appsync-events.ts +++ b/packages/event-handler/src/types/appsync-events.ts @@ -1,8 +1,47 @@ import type { Context } from 'aws-lambda'; +import type { AppSyncEventsResolver } from '../appsync-events/AppSyncEventsResolver.js'; import type { RouteHandlerRegistry } from '../appsync-events/RouteHandlerRegistry.js'; import type { Router } from '../appsync-events/Router.js'; import type { Anything, GenericLogger } from './common.js'; +// #region resolve options + +/** + * Optional object to pass to the {@link AppSyncEventsResolver.resolve | `AppSyncEventsResolver.resolve()`} method. + */ +type ResolveOptions = { + /** + * Reference to `this` instance of the class that is calling the `resolve` method. + * + * This parameter should be used only when using {@link AppSyncEventsResolver.onPublish | `AppSyncEventsResolver.onPublish()`} + * and {@link AppSyncEventsResolver.onSubscribe | `AppSyncEventsResolver.onSubscribe()`} as class method decorators, and + * it's used to bind the decorated methods to your class instance. + * + * @example + * ```ts + * import { AppSyncEventsResolver } from '@aws-lambda-powertools/event-handler/appsync-events'; + * + * const app = new AppSyncEventsResolver(); + * + * class Lambda { + * public scope = 'scoped'; + * + * ⁣@app.onPublish('/foo') + * public async handleFoo(payload: string) { + * return `${this.scope} ${payload}`; + * } + * + * public async handler(event: unknown, context: Context) { + * return app.resolve(event, context, { scope: this }); + * } + * } + * const lambda = new Lambda(); + * const handler = lambda.handler.bind(lambda); + * ``` + */ + scope?: unknown; +}; + // #region OnPublish fn type OnPublishHandlerFn = ( @@ -17,11 +56,13 @@ type OnPublishHandlerSyncFn = ( context: Context ) => unknown; +type OnPublishAggregatePayload = Array<{ + payload: Anything; + id: string; +}>; + type OnPublishHandlerAggregateFn = ( - events: Array<{ - payload: Anything; - id: string; - }>, + events: OnPublishAggregatePayload, event: AppSyncEventsPublishEvent, context: Context ) => Promise; @@ -294,8 +335,10 @@ export type { OnPublishHandlerSyncFn, OnPublishHandlerSyncAggregateFn, OnPublishHandlerAggregateFn, + OnPublishAggregatePayload, OnSubscribeHandler, OnPublishAggregateOutput, OnPublishEventPayload, OnPublishOutput, + ResolveOptions, }; diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index dfa9f78985..f43b4c8de3 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -3,10 +3,12 @@ export type { AppSyncEventsPublishEvent, AppSyncEventsSubscribeEvent, OnPublishAggregateOutput, + OnPublishAggregatePayload, OnPublishEventPayload, OnPublishOutput, RouteOptions, RouterOptions, + ResolveOptions, } from './appsync-events.js'; export type { diff --git a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index 0e153da91e..d7cff18285 100644 --- a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -5,7 +5,11 @@ import { AppSyncEventsResolver, UnauthorizedException, } from '../../../src/appsync-events/index.js'; -import type { AppSyncEventsSubscribeEvent } from '../../../src/types/appsync-events.js'; +import type { + AppSyncEventsSubscribeEvent, + OnPublishAggregatePayload, + OnPublishHandlerAggregateFn, +} from '../../../src/types/appsync-events.js'; import { onPublishEventFactory, onSubscribeEventFactory, @@ -65,58 +69,77 @@ describe('Class: AppSyncEventsResolver', () => { }); }); - it('preserves the scope when decorating methods', async () => { - // Prepare - const app = new AppSyncEventsResolver({ logger: console }); + it.each([ + { aggregate: true, channel: { path: '/foo', segments: ['foo'] } }, + { + aggregate: false, + channel: { + path: '/bar', + segments: ['bar'], + }, + }, + ])( + 'preserves the scope when decorating with onPublish aggregate=$aggregate', + async ({ aggregate, channel }) => { + // Prepare + const app = new AppSyncEventsResolver({ logger: console }); - class Lambda { - public scope = 'scoped'; + class Lambda { + public scope = 'scoped'; + + @app.onPublish('/foo', { aggregate }) + public async handleFoo(payloads: OnPublishAggregatePayload) { + return payloads.map((payload) => { + return { + id: payload.id, + payload: `${this.scope} ${payload.payload}`, + }; + }); + } - @app.onPublish('/foo') - public async handleFoo(payload: string) { - return `${this.scope} ${payload}`; - } + @app.onPublish('/bar') + public async handleBar(payload: string) { + return `${this.scope} ${payload}`; + } - public async handler(event: unknown, context: Context) { - return this.stuff(event, context); - } + public async handler(event: unknown, context: Context) { + return this.stuff(event, context); + } - async stuff(event: unknown, context: Context) { - return app.resolve(event, context); + async stuff(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } } - } - const lambda = new Lambda(); - const handler = lambda.handler.bind(lambda); + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); - // Act - const result = await handler( - onPublishEventFactory( - [ + // Act + const result = await handler( + onPublishEventFactory( + [ + { + id: '1', + payload: 'foo', + }, + ], + channel + ), + context + ); + + // Assess + expect(result).toEqual({ + events: [ { id: '1', - payload: 'foo', + payload: 'scoped foo', }, ], - { - path: '/foo', - segments: ['foo'], - } - ), - context - ); - - // Assess - expect(result).toEqual({ - events: [ - { - id: '1', - payload: 'scoped foo', - }, - ], - }); - }); + }); + } + ); - it('preserves the scope when decorating methods', async () => { + it('preserves the scope when decorating with onSubscribe', async () => { // Prepare const app = new AppSyncEventsResolver({ logger: console }); @@ -133,7 +156,7 @@ describe('Class: AppSyncEventsResolver', () => { } async stuff(event: unknown, context: Context) { - return app.resolve(event, context); + return app.resolve(event, context, { scope: this }); } } const lambda = new Lambda(); From a0e58d9db837fec3907a2237075a43eaa11d7aef Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 12 Jun 2025 10:21:19 +0200 Subject: [PATCH 5/5] chore: sonar findings --- .../src/appsync-events/AppSyncEventsResolver.ts | 6 +++--- .../tests/unit/appsync-events/AppSyncEventsResolver.test.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts index efe473eb7e..d889361104 100644 --- a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts +++ b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts @@ -125,7 +125,7 @@ class AppSyncEventsResolver extends Router { try { return { events: await (handler as OnPublishHandlerAggregateFn).apply( - options?.scope || this, + options?.scope ?? this, [event.events, event, context] ), }; @@ -141,7 +141,7 @@ class AppSyncEventsResolver extends Router { const { id, payload } = message; try { const result = await (handler as OnPublishHandlerFn).apply( - options?.scope || this, + options?.scope ?? this, [payload, event, context] ); return { @@ -183,7 +183,7 @@ class AppSyncEventsResolver extends Router { } const { handler } = routeHandlerOptions; try { - await (handler as OnSubscribeHandler).apply(options?.scope || this, [ + await (handler as OnSubscribeHandler).apply(options?.scope ?? this, [ event, context, ]); diff --git a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts index d7cff18285..16da12ce63 100644 --- a/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-events/AppSyncEventsResolver.test.ts @@ -8,7 +8,6 @@ import { import type { AppSyncEventsSubscribeEvent, OnPublishAggregatePayload, - OnPublishHandlerAggregateFn, } from '../../../src/types/appsync-events.js'; import { onPublishEventFactory,