Hooks
Hooks allow you to invoke a composable local or remote function on a targeted node.
Some use cases for the hooks
include:
Authenticating a user before all operations
Checking for an authorization token before making a request
You cannot use hooks to modify the request or the response. If you want to manipulate data, we recommend that you use custom resolvers.
Hooks increase processing time. Use them sparingly if processing time is important. Hooks are executed in the order you provide them. However, any blocking
hooks execute before non-blocking hooks.
Hook arguments
Hooks are plugins that accept the following arguments:
Copied to your clipboard"hooks": {"beforeAll": {"composer": "<Local or Remote file>","blocking": true|false}}
composer
(string) - The local or remote file location of the function you want to execute.You must add any local scripts to the mesh's
files
array. Local vs remote functions describes when to use a local or remote function.NOTE: Local composer functions are limited to 30 seconds. If
blocking
is set totrue
and the function takes longer than 30 seconds, you will receive aTimeout Error
. If you repeatedly encounter this error, consider using a remote composer.blocking
(boolean) - (false
by default) Determines if the query waits for a successful return message before continuing the query.The
blocking
argument allows you to stop running hooks for a query that does not receive a successful response.If blocking is
true
and the composer returns an error, all future hook executions are canceled.If blocking is
false
and the composer returns an error, the composer will still be invoked.
Hook payload
All hooks receive the following payload. Specific hooks extend their types based on additional data they may provide in the payload as described in the creating composers section.
Copied to your clipboard// Logger utilityinterface Logger {debug: (...args: any[]) => void;info: (...args: any[]) => void;warn: (...args: any[]) => void;error: (...args: any[]) => void;}// State API interface for managing key-value pairsexport interface StateApi {/*** Get a value by key* @param key Key to retrieve*/get(key: string): Promise<string | null>;/*** Put a key-value pair with optional TTL* @param key Key to store* @param value Value to store* @param config Optional configuration object that may contain a TTL value in seconds*/put(key: string, value: string, config?: { ttl?: number }): Promise<void>;/*** Delete a key-value pair.* @param key Key to delete.*/delete(key: string): Promise<void>;}// Context available within a hook function payload.interface HookPayloadContext {// Request from the clientrequest: Request;// GraphQL parametersparams: GraphQLParams;// Request bodybody?: unknown;// Request headersheaders?: Record<string, string>;// Secrets (Local hooks only)secrets?: Record<string, string>;// State API (Local hooks only)state?: StateApi;// Common logger (Local hooks only)logger?: Logger;}// Payload that all hook functions receiveinterface HookPayload {context: HookFunctionPayloadContext;// GraphQL document nodedocument?: DocumentNode;};// Payload that all source hook functions receive, including beforeSource and afterSource.interface SourceHookPayload extends HookPayload {// Name of the sourcesourceName?: string;};
The secrets
, state
, and logger
contexts are not available in remote composers.
Hook response
Hooks have the following response. Response information for specific hooks is described in the Creating composers section.
Copied to your clipboard/*** Hook response status.*/export enum HookResponseStatus {SUCCESS = 'SUCCESS',ERROR = 'ERROR',}/*** Response from a hook.*/interface HookResponse {status: HookResponseStatus;message: string;}
Types of hooks
beforeAll
The beforeAll
hook allows you to insert a function before the query takes place. This is a good place to add an authentication layer or anything else you want to run before your query.
The beforeAll
hook is a singular hook.
Copied to your clipboard"plugins": [{"hooks": {"beforeAll": {"composer": "./hooks.js#checkAuthHeader","blocking": true}}}],
afterAll
The afterAll
hook allows you to insert a function after the entire operation resolves, but before the response is returned.
afterAll
hooks allow a user to provide a function or an endpoint to invoke after executing the operation. afterAll
hooks can be used for logging or triggering events. Each hook can be blocking or non-blocking. Non-blocking hooks will not wait for the completion of the execution.
afterAll
hooks cannot be blocked because the data has already resolved.
Copied to your clipboardinterface AfterAllTransformObject {composer: string;}
beforeSource
The beforeSource
hook allows you to insert a function before querying a specific source. This is useful for adding source-specific authentication, logging, or request modification before making requests to individual GraphQL sources.
The beforeSource
hook uses source names as keys in the configuration object to specify which source the hook should target.
Copied to your clipboard"hooks": {"beforeSource": {"source1": [{"composer": "<Local or Remote file>","blocking": true},{"composer": "<Local or Remote file>","blocking": true}],"source2": [{"composer": "<Local or Remote file>","blocking": false},{"composer": "<Local or Remote file>","blocking": false}]}}
Copied to your clipboardinterface BeforeSourceTransformObject {[sourceName: string]: Array<{composer: string;blocking: boolean;}>;}
afterSource
The afterSource
hook allows you to insert a function after querying a specific source, but before returning the response. This is useful for logging source responses, transforming data, or triggering events after source operations complete.
The afterSource
hook uses source names as keys in the configuration object to specify which source the hook should target.
afterSource
hooks additionally support blocking behavior to control whether the response waits for the hook to complete.
Copied to your clipboard"hooks": {"afterSource": {"source1": [{"composer": "<Local or Remote file>","blocking": true},{"composer": "<Local or Remote file>","blocking": true}],"source2": [{"composer": "<Local or Remote file>","blocking": false},{"composer": "<Local or Remote file>","blocking": false}]}}
Copied to your clipboardinterface AfterSourceTransformObject {[sourceName: string]: Array<{composer: string;blocking: boolean;}>;}
Local vs remote functions
local
composers are defined within your mesh.json
file, whereas remote
composers are only referenced within your mesh file. Local and remote composers have different advantages and limitations.
local
composers
Use local composers if:
The entire operation will take less than 30 seconds.
The composer logic is simple and only requires access to the headers, body, and other context objects.
Avoid using local composers if:
The entire operation will take more than 30 seconds.
The composer needs to make network calls.
The composer has complex or nested loops.
The function uses restricted constructs, including:
alert
,debugger
,eval
,new Function()
,process
,setInterval
,setTimeout
,WebAssembly
, orwindow
.
Local composers require adding any local scripts to the mesh's files
array.
Copied to your clipboard{"meshConfig": {"sources": [{"name": "Commerce","handler": {"graphql": {"endpoint": "https://venia.magento.com/graphql"}}}],"plugins": [{"hooks": {"beforeAll": {"composer": "./hooks.js#checkAuthHeader","blocking": true}}}],"files": [{"path": "./hooks.js","content": <FILE CONTENT>}]}}
Fetching from remote origins
Local composers also support fetching from remote origins using fetch()
.
The following example could be used as a beforeAll
hook that validates an authorization token against a remote authorization endpoint using fetch()
.
Copied to your clipboardmodule.exports = {validateToken: async ({ context }) => {const { headers } = context;const { authorization } = headers;if (!authorization) {return {status: "ERROR",message: "Authorization header is missing",};}try {// Validate the token against a remote authorization serviceconst response = await fetch("https://auth.adobe.com/validate", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify({ token: authorization.replace("Bearer ", "") }),});const result = await response.json();if (!response.ok || !result.valid) {return {status: "ERROR",message: "Invalid authorization token",};}return {status: "SUCCESS",message: "Token validated successfully",data: {headers: {"x-user-id": result.userId,},},};} catch (error) {return {status: "ERROR",message: `Token validation failed: ${error.message}`,};}},};
remote
composers
If a local composer does not work or causes timeout errors, consider using a remote composer.
When using remote
composers, you could see decreased performance, because remote
composers add a network hop.
remote
composers can use the params
, context
, and document
arguments over the network. However, the serialization and deserialization of JSON data means that any complex fields or references will be lost. If the composer depends on complex fields or references, consider using a local
composer instead.
Example
Copied to your clipboard{"meshConfig": {"sources": [{"name": "Commerce","handler": {"graphql": {"endpoint": "https://venia.magento.com/graphql"}}}],"plugins": [{"hooks": {"beforeAll": {"composer": "<Remote Composer URL>","blocking": true}}}]}}
Creating composers
A composer can be a local function or a remote serverless function. Composer signatures differ depending on the hook used and the location of the function.
beforeAll
hooks
beforeAll
hooks have the following payload and response:
Copied to your clipboardinterface HookPayload {context: HookFunctionPayloadContext;// GraphQL document node.document?: DocumentNode;};
Copied to your clipboardinterface BeforeAllHookResponse extends HookResponse {data?: {headers?: {[headerName: string]: string;};};}
Since the beforeAll
hook runs at the root level, the document
object is empty ({}
) by default.
If the composer
is a remote function, all the arguments are sent in the POST
body when calling the function.
Due to the limitations of JSON
serialization and de-serialization, some complex JSON
fields inside a remote function's arguments might not function correctly over the HTTPS
call.
Local composer example
This simple composer checks for an authorization header before processing the query.
Copied to your clipboardmodule.exports = {isAuth: ({context}) => {if (!context.headers.authorization) {return {status: "ERROR",message: "Unauthorized",};}return {status: "SUCCESS",message: "Authorized",};},};
This remote composer fetches your authorization token and inserts it into the x-auth-token
header.
Copied to your clipboardfunction getToken({ authorization = "", body = "", url = "" }) {return `${authorization} - ${body} - ${url}`;}module.exports = {insertToken: ({ context }) => {const { headers, request, body } = context;const { authorization } = headers;const { url } = request;const authToken = getToken({ authorization, url, body });return {status: "SUCCESS",message: "Authorized",data: {headers: {"x-auth-token": authToken,},},};},};
Remote composer example
The following example remote composer checks for an authorization
header.
While this example uses Fastly Edge computing, you can use any serverless function with remote hooks.
Copied to your clipboardaddEventListener("fetch", (event) => event.respondWith(handleRequest(event)));async function handleRequest(event) {try {const body = await event.request.json();if (!body.context.headers["authorization"]) {return new Response({status: "SUCCESS",message: "Unauthorized"}, {status: 401});}return new Response({status: "SUCCESS",message: "Authorized"}, {status: 200});} catch (err) {return new Response(err, {status: 500});}}
afterAll
hooks
afterAll
hook composers have the following payload and response:
Copied to your clipboardinterface AfterAllHookPayload extends HookPayload {// GraphQL result to be returned to the client. Includes data, errors, and extensions.result: GraphQLResult;}
Copied to your clipboardinterface AfterAllHookResponse extends HookResponse {data?: {result?: GraphQLResult;};}
afterAll
hook composers can be local or remote.
Due to the limitations of JSON
serialization and de-serialization, some complex JSON
fields inside a remote function's arguments might not function correctly over the HTTPS
call.
Local hook functions have a 30-second timeout. If a local hook function takes longer than 30 seconds, it will timeout and return an error. Non-blocking hooks will not cause the operation to fail even if they timeout.
Examples
Copied to your clipboardmodule.exports = {metaData: async (payload) => {const originalData = payload.result?.data || {};const originalErrors = payload.result?.errors || [];console.log('AfterAll Hook: Adding simple audit trail');// Extract dynamic information from the GraphQL request/responseconst queriedFields = Object.keys(originalData);const primaryQuery = queriedFields.length > 0 ? queriedFields[0] : 'unknown';const queryDocument = payload.document || '';const operationType = queryDocument.toString().includes('mutation') ? 'mutation' : 'query';// Calculate response sizeconst responseSize = JSON.stringify(originalData).length;// Add comprehensive dynamic audit metadataconst extensions = {_metaData: {primaryQuery: primaryQuery,operationType: operationType,responseSizeBytes: responseSize,processedBy: 'local-hook'}};return {status: 'SUCCESS',message: `Audit trail added for ${primaryQuery} ${operationType}`,data: {result: {data: originalData,errors: originalErrors,extensions,}}};},};
Copied to your clipboardaddEventListener("fetch", (event) => event.respondWith(handleRequest(event)));async function handleRequest(event) {try {const payload = await event.request.json();const { result, context, document } = payload;// Add a new 'sale_price' field that provides 20% discount for all productsif (result.data?.products?.items) {result.data.products.items.forEach(product => {if (product.price_range?.minimum_price?.final_price?.value) {const originalPrice = product.price_range.minimum_price.final_price.value;const salePrice = originalPrice * 0.8; // 20% discountproduct.sale_price = {value: Math.round(salePrice * 100) / 100,currency: product.price_range.minimum_price.final_price.currency,discount_percent: 20};}});}return new Response(JSON.stringify({status: "SUCCESS",message: "Price modification applied",data: {result: {data: result.data,errors: result.errors || []}}}),{ status: 200 });} catch (err) {return new Response(JSON.stringify({status: "ERROR",message: err.message}),{ status: 500 });}}
beforeSource
hooks
beforeSource
hooks have the following payload and response:
Copied to your clipboardinterface BeforeSourceHookPayload extends SourceHookPayload {// Request init to be used in the source fetch request. Includes body, headers, method.request: RequestInit;}
Copied to your clipboardinterface BeforeSourceHookResponse extends HookResponse {data?: {request?:| RequestInit| {body?: string | ReadableStream<Uint8Array>;headers?: Record<string, string>;method?: string;url?: string;};};}
beforeSource
hook composers can be local or remote. You can configure multiple hooks for each source, which execute in the specified order.
Examples
The local composer example adds source-specific headers before making requests to the Adobe Commerce API. The remote composer example validates source-specific authentication before making requests.
Copied to your clipboardmodule.exports = {beforeCommerceRequest: ({ sourceName, request, operation }) => {// Add Commerce-specific authentication headersconst commerceHeaders = {"x-commerce-store": "default","x-commerce-customer-token": request.headers?.authorization?.replace("Bearer ", "") || "",};return {status: "SUCCESS",message: "Commerce headers added",data: {request: {headers: commerceHeaders,}},};},};
Copied to your clipboardaddEventListener("fetch", (event) => event.respondWith(handleRequest(event)));async function handleRequest(event) {try {const { sourceName, request, operation } = await event.request.json();// Validate source-specific authenticationif (sourceName === "CommerceApi" && !request.headers["x-commerce-token"]) {return new Response(JSON.stringify({status: "ERROR",message: "Commerce token required for this source",}),{ status: 401 });}return new Response(JSON.stringify({status: "SUCCESS",message: "Source validation passed",}),{ status: 200 });} catch (err) {return new Response(JSON.stringify({status: "ERROR",message: err.message,}),{ status: 500 });}}
afterSource
hooks
afterSource
hooks have the following payload and response:
Copied to your clipboardinterface AfterSourceHookPayload extends SourceHookPayload {// Response from the source fetch request. Includes body, headers, status, statusText.response: Response;}
Copied to your clipboardinterface AfterSourceHookResponse extends HookResponse {data?: {response?:| Response| {body?: string | ReadableStream<Uint8Array>;headers?: Record<string, string>;status?: number;statusText?: string;};};}
afterSource
hook composers can be local or remote. Multiple hooks can be configured for each source, and they will be executed in order.
Examples
The local composer example logs source responses and modifies the response after source operations. The remote composer example publishes events after source operations complete.
Copied to your clipboardmodule.exports = {afterCommerceResponse: ({ sourceName, request, operation, response, setResponse }) => {console.log(`Source ${sourceName} returned response:`, response);// Modify the response if neededif (sourceName === "Commerce") {// Example: Add custom headers to the responseconst modifiedResponse = new Response(response.body, {status: response.status,statusText: response.statusText,headers: {...Object.fromEntries(response.headers.entries()),"x-processed-by": "commerce-hook",},});return {status: "SUCCESS",message: "Source response processed",data: {response: modifiedResponse,}};}return {status: "SUCCESS",message: "Source response processed",};},};
Copied to your clipboardaddEventListener("fetch", (event) => event.respondWith(handleRequest(event)));async function handleRequest(event) {try {const { sourceName, request, operation, response } = await event.request.json();// Publish source completion eventawait fetch("https://events.adobe.com/publish", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify({event: "source_completed",sourceName,timestamp: new Date().toISOString(),responseStatus: response.status,}),});return new Response(JSON.stringify({status: "SUCCESS",message: "Source event published",}),{ status: 200 });} catch (err) {return new Response(JSON.stringify({status: "ERROR",message: err.message,}),{ status: 500 });}}
Return signatures
The return signature of a composer is the same for local and remote functions.
Copied to your clipboard{status: "ERROR" | "SUCCESS",message: string,data?: {headers?: {[headerName: string]: string}}}
onFetch
hooks
You can use the onFetch
plugin to intercept and modify HTTP requests before they are sent to your GraphQL sources.
The onFetch
plugin can assist with the following use cases:
- Authentication: Adding dynamic auth tokens or API keys
- Conditional Headers: Adding headers based on query content or user context
- Request Tracking: Adding correlation IDs or request timestamps
- Request Modification: Transforming request body or parameters
- Logging: Adding custom logging or metrics
The onFetch
plugin can also access your execution parameters, such as: root
, args
, context
, and info
.
The following example adds a custom header (x-md5-hash
) to the request. This could be used to add a hash of the request body to the request headers for security purposes.
mesh.json
handleOnFetch.js
Copied to your clipboard{"meshConfig": {"sources": [{"name": "CommerceAPI","handler": {"graphql": {"endpoint": "https://venia.magento.com/graphql"}}}],"plugins": [{"onFetch": [{"source": "commerceAPI","handler": "./handleOnFetch.js"}]}]}}
Copied to your clipboardasync function handleOnFetch(data) {const { context } = data;const { log } = context;try {data.options.headers["x-md5-hash"] = "test header value";} catch (e) {log(`Error setting hash header: ${e.message}`);}}module.exports = {default: handleOnFetch,__esModule: true,};
context.logger
context.logger
is only available in local hooks. For remote hooks, use language-specific logging, such as console.log
in JavaScript.
The context
object provides access to a logger
that you can use for debugging and monitoring.
Copied to your clipboard// In local functionscontext.logger.log("Processing request");context.logger.error("An error occurred");context.logger.warn("Warning message");
context.logger
has the following limitations:
- Character limit: Log messages are limited to 100 characters.
- Local functions only: The logger is only available in local functions.
- Log levels: The logger supports the following log levels:
log
,error
,warn
.
Example
The following example hook checks for authentication before processing GraphQL requests.
Copied to your clipboardmodule.exports = {checkAuth: ({ context }) => {context.logger.log("Checking authentication");try {const authHeader = context.headers.authorization;if (!authHeader) {context.logger.error("No authorization header found");return {status: "ERROR",message: "Unauthorized - missing token"};}context.logger.log("Authentication check completed");return {status: "SUCCESS",message: "Authorized"};} catch (error) {context.logger.error("Authentication check failed");return {status: "ERROR",message: "Authentication error"};}}};