-
Notifications
You must be signed in to change notification settings - Fork 156
feat(event-handler): add single resolver functionality for AppSync GraphQL API #3999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
arnabrahman
wants to merge
30
commits into
aws-powertools:main
Choose a base branch
from
arnabrahman:1166-graphql-resolver
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
951dba3
feat: implement `RouteHandlerRegistry` for managing GraphQL route han…
arnabrahman 7d9a27e
feat: add type guard for AppSync GraphQL event validation
arnabrahman 69c6210
refactor: simplify handler function signatures and update type defini…
arnabrahman e5762f3
refactor: remove wrong `result` property check from AppSync GraphQL e…
arnabrahman 7b9d1f9
feat: implement Router class for managing `Query` events for appsync …
arnabrahman 707dc1c
feat: add `onMutation` method for handling GraphQL Mutation events in…
arnabrahman a71aead
feat: implement AppSyncGraphQLResolver class to handle onQuery and on…
arnabrahman c4f6b8d
doc: `#executeSingleResolver` function
arnabrahman 387f5cf
feat: add warning for unimplemented batch resolvers in AppSyncGraphQL…
arnabrahman 70b9921
feat: enhance `RouteHandlerRegistry` to log handler registration and …
arnabrahman 286096b
feat: add `onQueryEventFactory` and `onMutationEventFactory` to creat…
arnabrahman 58e3b26
feat: add unit tests for `AppSyncGraphQLResolver` class to validate e…
arnabrahman d821b3d
feat: add unit tests for `RouteHandlerRegistry` to validate handler r…
arnabrahman 3076c1c
feat: add unit tests for `Router` class to validate resolver registra…
arnabrahman ff85c24
feat: add test for nested resolvers registration using the decorator …
arnabrahman 0d5c893
feat: enhance documentation for `resolve` method in `AppSyncGraphQLRe…
arnabrahman 700c779
chore: warning message for batch resolver
arnabrahman 5580923
fix: return query handler if found
arnabrahman 7e8aa10
fix: correct warning message for batch resolver in AppSyncGraphQLReso…
arnabrahman 80a11ff
fix: update debug messages to reflect resolver registration format in…
arnabrahman e8b0db7
fix: update resolver not found messages for consistency in AppSyncGra…
arnabrahman 65c8ee2
fix: doc for Router
arnabrahman f1b545e
refactor: remove unused cache and warning set from `RouteHandlerRegis…
arnabrahman 11a0443
fix: update documentation for resolve method in RouteHandlerRegistry
arnabrahman 0e9d2ca
refactor: remove redundant test for cached route handler evaluation i…
arnabrahman 99531aa
fix: update import path for Router in Router.test.ts
arnabrahman 1342ca4
fix: update debug messages to include event type in RouteHandlerRegis…
arnabrahman e5454c9
fix: update terminology from "handler" to "resolver" in RouteHandlerR…
arnabrahman c855e53
Merge branch 'main' into 1166-graphql-resolver
arnabrahman 881138f
fix: refactor logger initialization and import structure in Router an…
arnabrahman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
161 changes: 161 additions & 0 deletions
161
packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import type { Context } from 'aws-lambda'; | ||
import type { AppSyncGraphQLEvent } from '../types/appsync-graphql.js'; | ||
import { Router } from './Router.js'; | ||
import { ResolverNotFoundException } from './errors.js'; | ||
import { isAppSyncGraphQLEvent } from './utils.js'; | ||
|
||
/** | ||
* Resolver for AWS AppSync GraphQL APIs. | ||
* | ||
* This resolver is designed to handle the `onQuery` and `onMutation` events | ||
* from AWS AppSync GraphQL APIs. It allows you to register handlers for these events | ||
* and route them to the appropriate functions based on the event's field & type. | ||
* | ||
* @example | ||
* ```ts | ||
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; | ||
* | ||
* const app = new AppSyncGraphQLResolver(); | ||
* | ||
* app.onQuery('getPost', async ({ id }) => { | ||
* // your business logic here | ||
* return { | ||
* id, | ||
* title: 'Post Title', | ||
* content: 'Post Content', | ||
* }; | ||
* }); | ||
* | ||
* export const handler = async (event, context) => | ||
* app.resolve(event, context); | ||
* ``` | ||
*/ | ||
export class AppSyncGraphQLResolver extends Router { | ||
/** | ||
* Resolve the response based on the provided event and route handlers configured. | ||
* | ||
* @example | ||
* ```ts | ||
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; | ||
* | ||
* const app = new AppSyncGraphQLResolver(); | ||
* | ||
* app.onQuery('getPost', async ({ id }) => { | ||
* // your business logic here | ||
* return { | ||
* id, | ||
* title: 'Post Title', | ||
* content: 'Post Content', | ||
* }; | ||
* }); | ||
* | ||
* export const handler = async (event, context) => | ||
* app.resolve(event, context); | ||
* ``` | ||
* | ||
* The method works also as class method decorator, so you can use it like this: | ||
* | ||
* @example | ||
* ```ts | ||
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; | ||
* | ||
* const app = new AppSyncGraphQLResolver(); | ||
* | ||
* class Lambda { | ||
* @app.onQuery('getPost') | ||
* async handleGetPost({ id }) { | ||
* // your business logic here | ||
* return { | ||
* id, | ||
* title: 'Post Title', | ||
* content: 'Post Content', | ||
* }; | ||
* } | ||
* | ||
* async handler(event, context) { | ||
* return app.resolve(event, context); | ||
* } | ||
* } | ||
* | ||
* const lambda = new Lambda(); | ||
* export const handler = lambda.handler.bind(lambda); | ||
* ``` | ||
* | ||
* @param event - The incoming event, which may be an AppSync GraphQL event or an array of events. | ||
* @param context - The Lambda execution context. | ||
*/ | ||
public async resolve(event: unknown, context: Context): Promise<unknown> { | ||
if (Array.isArray(event)) { | ||
this.logger.warn('Batch resolver is not implemented yet'); | ||
return; | ||
} | ||
if (!isAppSyncGraphQLEvent(event)) { | ||
this.logger.warn( | ||
'Received an event that is not compatible with this resolver' | ||
); | ||
return; | ||
} | ||
try { | ||
return await this.#executeSingleResolver(event); | ||
} catch (error) { | ||
this.logger.error( | ||
`An error occurred in handler ${event.info.fieldName}`, | ||
error | ||
); | ||
if (error instanceof ResolverNotFoundException) throw error; | ||
return this.#formatErrorResponse(error); | ||
} | ||
} | ||
|
||
/** | ||
* Executes the appropriate resolver (query or mutation) for a given AppSync GraphQL event. | ||
* | ||
* This method attempts to resolve the handler for the specified field and type name | ||
* from the query and mutation registries. If a matching handler is found, it invokes | ||
* the handler with the event arguments. If no handler is found, it throws a | ||
* `ResolverNotFoundException`. | ||
* | ||
* @param event - The AppSync GraphQL event containing resolver information. | ||
* @throws {ResolverNotFoundException} If no resolver is registered for the given field and type. | ||
*/ | ||
async #executeSingleResolver(event: AppSyncGraphQLEvent): Promise<unknown> { | ||
const { fieldName, parentTypeName: typeName } = event.info; | ||
const queryHandlerOptions = this.onQueryRegistry.resolve( | ||
typeName, | ||
fieldName | ||
); | ||
if (queryHandlerOptions) { | ||
return await queryHandlerOptions.handler.apply(this, [event.arguments]); | ||
} | ||
|
||
const mutationHandlerOptions = this.onMutationRegistry.resolve( | ||
typeName, | ||
fieldName | ||
); | ||
if (mutationHandlerOptions) { | ||
return await mutationHandlerOptions.handler.apply(this, [ | ||
event.arguments, | ||
]); | ||
} | ||
|
||
throw new ResolverNotFoundException( | ||
`No resolver found for ${typeName}-${fieldName}` | ||
); | ||
} | ||
|
||
/** | ||
* Format the error response to be returned to the client. | ||
* | ||
* @param error - The error object | ||
*/ | ||
#formatErrorResponse(error: unknown) { | ||
if (error instanceof Error) { | ||
return { | ||
error: `${error.name} - ${error.message}`, | ||
}; | ||
} | ||
return { | ||
error: 'An unknown error occurred', | ||
}; | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import type { | ||
GenericLogger, | ||
RouteHandlerOptions, | ||
RouteHandlerRegistryOptions, | ||
} from '../types/appsync-graphql.js'; | ||
|
||
/** | ||
* Registry for storing route handlers for the `query` and `mutation` events in AWS AppSync GraphQL API's. | ||
* | ||
* This class should not be used directly unless you are implementing a custom router. | ||
* Instead, use the {@link Router} class, which is the recommended way to register routes. | ||
*/ | ||
class RouteHandlerRegistry { | ||
/** | ||
* A map of registered route handlers, keyed by their type & field name. | ||
*/ | ||
protected readonly resolvers: Map<string, RouteHandlerOptions> = new Map(); | ||
/** | ||
* A logger instance to be used for logging debug and warning messages. | ||
*/ | ||
readonly #logger: GenericLogger; | ||
/** | ||
* The event type stored in the registry. | ||
*/ | ||
readonly #eventType: 'onQuery' | 'onMutation'; | ||
|
||
public constructor(options: RouteHandlerRegistryOptions) { | ||
this.#logger = options.logger; | ||
this.#eventType = options.eventType ?? 'onQuery'; | ||
} | ||
|
||
/** | ||
* Registers a new GraphQL route resolver for a specific type and field. | ||
* | ||
* @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function. | ||
* @param options.fieldName - The field name of the GraphQL type to be registered | ||
* @param options.handler - The handler function to be called when the GraphQL event is received | ||
* @param options.typeName - The name of the GraphQL type to be registered | ||
* | ||
*/ | ||
public register(options: RouteHandlerOptions): void { | ||
const { fieldName, handler, typeName } = options; | ||
this.#logger.debug( | ||
`Adding ${this.#eventType} resolver for field ${typeName}.${fieldName}` | ||
); | ||
const cacheKey = this.#makeKey(typeName, fieldName); | ||
if (this.resolvers.has(cacheKey)) { | ||
this.#logger.warn( | ||
`A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.` | ||
); | ||
} | ||
this.resolvers.set(cacheKey, { | ||
fieldName, | ||
handler, | ||
typeName, | ||
}); | ||
} | ||
|
||
/** | ||
* Resolves the handler for a specific GraphQL API event. | ||
* | ||
* @param typeName - The name of the GraphQL type (e.g., "Query", "Mutation", or a custom type). | ||
* @param fieldName - The name of the field within the specified type. | ||
*/ | ||
public resolve( | ||
typeName: string, | ||
fieldName: string | ||
): RouteHandlerOptions | undefined { | ||
this.#logger.debug( | ||
`Looking for ${this.#eventType} resolver for type=${typeName}, field=${fieldName}` | ||
); | ||
return this.resolvers.get(this.#makeKey(typeName, fieldName)); | ||
} | ||
|
||
/** | ||
* Generates a unique key by combining the provided GraphQL type name and field name. | ||
* | ||
* @param typeName - The name of the GraphQL type. | ||
* @param fieldName - The name of the GraphQL field. | ||
*/ | ||
#makeKey(typeName: string, fieldName: string): string { | ||
return `${typeName}.${fieldName}`; | ||
} | ||
} | ||
|
||
export { RouteHandlerRegistry }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The caller of this function does the awaiting so there's no need to await here. Likewise on L136