Skip to content

Commit b91383f

Browse files
authored
feat(event-handler): add single resolver functionality for AppSync GraphQL API (#3999)
1 parent 293bbd5 commit b91383f

File tree

12 files changed

+1433
-1
lines changed

12 files changed

+1433
-1
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import type { AppSyncResolverEvent, Context } from 'aws-lambda';
2+
import type { ResolveOptions } from '../types/appsync-graphql.js';
3+
import { Router } from './Router.js';
4+
import { ResolverNotFoundException } from './errors.js';
5+
import { isAppSyncGraphQLEvent } from './utils.js';
6+
7+
/**
8+
* Resolver for AWS AppSync GraphQL APIs.
9+
*
10+
* This resolver is designed to handle GraphQL events from AWS AppSync GraphQL APIs. It allows you to register handlers for these events
11+
* and route them to the appropriate functions based on the event's field & type.
12+
*
13+
* @example
14+
* ```ts
15+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
16+
*
17+
* const app = new AppSyncGraphQLResolver();
18+
*
19+
* app.resolver(async ({ id }) => {
20+
* // your business logic here
21+
* return {
22+
* id,
23+
* title: 'Post Title',
24+
* content: 'Post Content',
25+
* };
26+
* }, {
27+
* fieldName: 'getPost',
28+
* typeName: 'Query'
29+
* });
30+
*
31+
* export const handler = async (event, context) =>
32+
* app.resolve(event, context);
33+
* ```
34+
*/
35+
export class AppSyncGraphQLResolver extends Router {
36+
/**
37+
* Resolve the response based on the provided event and route handlers configured.
38+
*
39+
* @example
40+
* ```ts
41+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
42+
*
43+
* const app = new AppSyncGraphQLResolver();
44+
*
45+
* app.resolver(async ({ id }) => {
46+
* // your business logic here
47+
* return {
48+
* id,
49+
* title: 'Post Title',
50+
* content: 'Post Content',
51+
* };
52+
* }, {
53+
* fieldName: 'getPost',
54+
* typeName: 'Query'
55+
* });
56+
*
57+
* export const handler = async (event, context) =>
58+
* app.resolve(event, context);
59+
* ```
60+
*
61+
* The method works also as class method decorator, so you can use it like this:
62+
*
63+
* @example
64+
* ```ts
65+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
66+
*
67+
* const app = new AppSyncGraphQLResolver();
68+
*
69+
* class Lambda {
70+
* ⁣@app.resolver({ fieldName: 'getPost', typeName: 'Query' })
71+
* async handleGetPost({ id }) {
72+
* // your business logic here
73+
* return {
74+
* id,
75+
* title: 'Post Title',
76+
* content: 'Post Content',
77+
* };
78+
* }
79+
*
80+
* async handler(event, context) {
81+
* return app.resolve(event, context, {
82+
* scope: this, // bind decorated methods to the class instance
83+
* });
84+
* }
85+
* }
86+
*
87+
* const lambda = new Lambda();
88+
* export const handler = lambda.handler.bind(lambda);
89+
* ```
90+
*
91+
* @param event - The incoming event, which may be an AppSync GraphQL event or an array of events.
92+
* @param context - The Lambda execution context.
93+
* @param options - Optional parameters for the resolver, such as the scope of the handler.
94+
*/
95+
public async resolve(
96+
event: unknown,
97+
context: Context,
98+
options?: ResolveOptions
99+
): Promise<unknown> {
100+
if (Array.isArray(event)) {
101+
this.logger.warn('Batch resolver is not implemented yet');
102+
return;
103+
}
104+
if (!isAppSyncGraphQLEvent(event)) {
105+
this.logger.warn(
106+
'Received an event that is not compatible with this resolver'
107+
);
108+
return;
109+
}
110+
try {
111+
return await this.#executeSingleResolver(event, context, options);
112+
} catch (error) {
113+
this.logger.error(
114+
`An error occurred in handler ${event.info.fieldName}`,
115+
error
116+
);
117+
if (error instanceof ResolverNotFoundException) throw error;
118+
return this.#formatErrorResponse(error);
119+
}
120+
}
121+
122+
/**
123+
* Executes the appropriate resolver for a given AppSync GraphQL event.
124+
*
125+
* This method attempts to resolve the handler for the specified field and type name
126+
* from the resolver registry. If a matching handler is found, it invokes the handler
127+
* with the event arguments. If no handler is found, it throws a `ResolverNotFoundException`.
128+
*
129+
* @param event - The AppSync resolver event containing the necessary information.
130+
* @param context - The Lambda execution context.
131+
* @param options - Optional parameters for the resolver, such as the scope of the handler.
132+
* @throws {ResolverNotFoundException} If no resolver is registered for the given field and type.
133+
*/
134+
async #executeSingleResolver(
135+
event: AppSyncResolverEvent<Record<string, unknown>>,
136+
context: Context,
137+
options?: ResolveOptions
138+
): Promise<unknown> {
139+
const { fieldName, parentTypeName: typeName } = event.info;
140+
141+
const resolverHandlerOptions = this.resolverRegistry.resolve(
142+
typeName,
143+
fieldName
144+
);
145+
if (resolverHandlerOptions) {
146+
return resolverHandlerOptions.handler.apply(options?.scope ?? this, [
147+
event.arguments,
148+
event,
149+
context,
150+
]);
151+
}
152+
153+
throw new ResolverNotFoundException(
154+
`No resolver found for ${typeName}-${fieldName}`
155+
);
156+
}
157+
158+
/**
159+
* Format the error response to be returned to the client.
160+
*
161+
* @param error - The error object
162+
*/
163+
#formatErrorResponse(error: unknown) {
164+
if (error instanceof Error) {
165+
return {
166+
error: `${error.name} - ${error.message}`,
167+
};
168+
}
169+
return {
170+
error: 'An unknown error occurred',
171+
};
172+
}
173+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type {
2+
GenericLogger,
3+
RouteHandlerOptions,
4+
RouteHandlerRegistryOptions,
5+
} from '../types/appsync-graphql.js';
6+
7+
/**
8+
* Registry for storing route handlers for GraphQL resolvers in AWS AppSync GraphQL API's.
9+
*
10+
* This class should not be used directly unless you are implementing a custom router.
11+
* Instead, use the {@link Router} class, which is the recommended way to register routes.
12+
*/
13+
class RouteHandlerRegistry {
14+
/**
15+
* A map of registered route handlers, keyed by their type & field name.
16+
*/
17+
protected readonly resolvers: Map<string, RouteHandlerOptions> = new Map();
18+
/**
19+
* A logger instance to be used for logging debug and warning messages.
20+
*/
21+
readonly #logger: GenericLogger;
22+
23+
public constructor(options: RouteHandlerRegistryOptions) {
24+
this.#logger = options.logger;
25+
}
26+
27+
/**
28+
* Registers a new GraphQL route resolver for a specific type and field.
29+
*
30+
* @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function.
31+
* @param options.fieldName - The field name of the GraphQL type to be registered
32+
* @param options.handler - The handler function to be called when the GraphQL event is received
33+
* @param options.typeName - The name of the GraphQL type to be registered
34+
*
35+
*/
36+
public register(options: RouteHandlerOptions): void {
37+
const { fieldName, handler, typeName } = options;
38+
this.#logger.debug(`Adding resolver for field ${typeName}.${fieldName}`);
39+
const cacheKey = this.#makeKey(typeName, fieldName);
40+
if (this.resolvers.has(cacheKey)) {
41+
this.#logger.warn(
42+
`A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.`
43+
);
44+
}
45+
this.resolvers.set(cacheKey, {
46+
fieldName,
47+
handler,
48+
typeName,
49+
});
50+
}
51+
52+
/**
53+
* Resolves the handler for a specific GraphQL API event.
54+
*
55+
* @param typeName - The name of the GraphQL type (e.g., "Query", "Mutation", or a custom type).
56+
* @param fieldName - The name of the field within the specified type.
57+
*/
58+
public resolve(
59+
typeName: string,
60+
fieldName: string
61+
): RouteHandlerOptions | undefined {
62+
this.#logger.debug(
63+
`Looking for resolver for type=${typeName}, field=${fieldName}`
64+
);
65+
return this.resolvers.get(this.#makeKey(typeName, fieldName));
66+
}
67+
68+
/**
69+
* Generates a unique key by combining the provided GraphQL type name and field name.
70+
*
71+
* @param typeName - The name of the GraphQL type.
72+
* @param fieldName - The name of the GraphQL field.
73+
*/
74+
#makeKey(typeName: string, fieldName: string): string {
75+
return `${typeName}.${fieldName}`;
76+
}
77+
}
78+
79+
export { RouteHandlerRegistry };

0 commit comments

Comments
 (0)