Hooks allow you to invoke composable local or remote functions on a targeted node.
Some use cases for Hooks include:
- Authenticating a user before all operations
- Checking for an authorization token before making a request
- Modifying request headers before fetching from a source
- Modifying response headers after fetching from a source
- Logging execution results after GraphQL operations
NOTE: Hooks increase processing time when blocking with duration based on their complexity. Use them sparingly if processing time is important.
Local and remote functions are defined in your configuration. Hooks are configured as plugins that accept the following arguments:
{
"hooks": {
"beforeAll": {
"composer": "<Local or Remote file>",
"blocking": true|false
},
"beforeSource": {
"sourceName": [
{
"composer": "<Local or Remote file>",
"blocking": true|false
}
]
},
"afterSource": {
"sourceName": [
{
"composer": "<Local or Remote file>",
"blocking": true|false
}
]
},
"afterAll": {
"composer": "<Local or Remote file>",
"blocking": true|false
}
}
}- composer (string) - The local or remote file location of the function you want to execute.
- blocking (boolean) - (false by default) Determines whether the query waits for a successful return message before continuing.
- sourceName (string) - For source hooks, the name of the source to target (e.g., "users", "products").
NOTE: Hooks are executed in the order configured, with blocking hooks running before non-blocking ones. Errors from non-blocking hooks are ignored.
Local and remote functions must return an object that conforms to the HookResponse interface.
interface HookResponse {
status: "SUCCESS" | "ERROR"
message: string,
data?: {
headers?: {
[headerName: string]: string
}
}
}The beforeAll hook executes once before any GraphQL operation. It has access to the request context and can modify headers.
Hook Function Signature:
interface BeforeAllHookFunctionPayload {
context: {
request: Request;
params: GraphQLParams;
secrets?: Record<string, string>;
state?: StateApi;
logger?: YogaLogger;
};
document?: unknown;
}
interface BeforeAllHookResponse extends HookResponse {
data?: {
headers?: {
[headerName: string]: string;
};
};
}Example:
module.exports = {
addAuthHeader: async ({ context }) => {
const { headers, secrets } = context;
if (!headers.authorization) {
return {
status: 'ERROR',
message: 'Authorization header required'
};
}
return {
status: 'SUCCESS',
message: 'Authorization validated',
data: {
headers: {
'X-Auth-Validated': 'true'
}
}
};
}
};The beforeSource hook executes once before fetching data from a named source. It has access to the request object, allowing you to modify request headers, body, or other request properties.
Hook Function Signature:
interface BeforeSourceHookFunctionPayload {
context: {
request: Request;
params: GraphQLParams;
secrets?: Record<string, string>;
state?: StateApi;
logger?: YogaLogger;
};
request: RequestInit; // Can be modified
document?: unknown;
sourceName: string;
}
interface BeforeSourceHookResponse extends HookResponse {
data?: {
request?: RequestInit | {
body?: string | ReadableStream<Uint8Array>;
headers?: Record<string, string>;
method?: string;
url?: string;
};
};
}Example:
module.exports = {
modifyRequest: async ({ context, request, sourceName }) => {
// Add authentication header to request
const modifiedRequest = {
...request,
headers: {
...request.headers,
'Authorization': `Bearer ${context.secrets.API_TOKEN}`
}
};
return {
status: 'SUCCESS',
message: 'Request modified successfully',
data: {
request: modifiedRequest
}
};
}
};The afterSource hook executes once after fetching data from a named source. It has access to the response object, allowing you to modify response headers, body, or other response properties.
Hook Function Signature:
interface AfterSourceHookFunctionPayload {
context: {
request: Request;
params: GraphQLParams;
secrets?: Record<string, string>;
state?: StateApi;
logger?: YogaLogger;
};
document?: unknown;
sourceName: string;
response?: Response; // Can be modified
}
interface AfterSourceHookResponse extends HookResponse {
data?: {
response?: Response | {
body?: string | ReadableStream<Uint8Array>;
headers?: Record<string, string>;
status?: number;
statusText?: string;
};
};
}Example:
module.exports = {
modifyResponse: async ({ context, response, sourceName }) => {
// Add custom header to response
const modifiedResponse = new Response(response.body, {
...response,
headers: {
...response.headers,
'X-Custom-Header': 'modified-by-hook'
}
});
return {
status: 'SUCCESS',
message: 'Response modified successfully',
data: {
response: modifiedResponse
}
};
}
};The afterAll hook executes once after GraphQL execution is complete, but before the final response is sent to the client. It has access to the execution result, allowing you to modify the final response or perform cleanup operations.
Hook Function Signature:
interface AfterAllHookFunctionPayload {
context: {
request: Request;
params: GraphQLParams;
secrets?: Record<string, string>;
state?: StateApi;
logger?: YogaLogger;
};
document?: unknown;
result?: GraphQLResult; // The final execution result
}
interface AfterAllHookResponse extends HookResponse {
data?: {
result?: GraphQLResult; // Can be modified
};
}Example:
module.exports = {
logExecution: async ({ context, result }) => {
// Log execution result
context.logger.info('GraphQL execution completed', {
hasErrors: result.errors && result.errors.length > 0,
dataKeys: result.data ? Object.keys(result.data) : []
});
return {
status: 'SUCCESS',
message: 'Execution logged successfully'
};
}
};Local functions are JavaScript functions that are bundled with and executed on the server. They should be written as CommonJS functions exported from the referenced hooks module, either a default or named export.
Avoid using local functions if:
- The entire operation will take more than 30 seconds.
- The function uses restricted constructs, including
process,window,debugger,alert,setTimeout,setInterval,new Function(),eval, orWebAssembly.
An example of a local function is shown below:
module.exports = {
/**
* Hook function to validate headers against context secret.
* @type {import('@adobe/plugin-hooks').HookFunction} Hook function
* @param {import('@adobe/plugin-hooks').HookFunctionPayload} Hook payload
* @returns {Promise<import('@adobe/plugin-hooks').HookResponse>} Hook response
*/
isAuth: async ({context}) => {
function test() {}
const {
headers,
secrets,
} = context;
test();
if (headers.authorization !== secrets.TOKEN) {
return {
status: 'ERROR',
message: "Unauthorized",
};
} else {
return {
status: "SUCCESS",
message: "Authorized",
};
}
},
}See examples for additional examples of local functions.
If a local function does not work or causes timeout errors, consider using a remote function.
You are free to use any language, framework, or library with remote functions, as long as they return a valid response.
A remote function must be served with the HTTPS protocol and be accessible from the internet. Requests to remote
functions use the POST HTTP method. Remote functions can increase latency due to the additional network hop involved.
Remote functions can use the params, context, and document arguments over the network. However, 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 function instead.
The package now provides both ESM and CommonJS outputs:
- ESM:
dist/esm/index.js - CommonJS:
dist/cjs/index.js - TypeScript declarations:
dist/types/index.d.ts
Before:
const hooksPlugin = require('@adobe/plugin-hooks');After:
// CommonJS
const hooksPlugin = require('@adobe/plugin-hooks');
// ESM
import hooksPlugin from '@adobe/plugin-hooks';Hook functions now receive properly typed payloads. The basic structure remains the same, but TypeScript users will get better type safety.
Before:
module.exports = {
myHook: async (payload) => {
// payload structure was loosely defined
}
};After:
module.exports = {
myHook: async (payload) => {
// payload is now properly typed
const { context, document } = payload;
const { headers, secrets, state } = context;
}
};Errors now use GraphQL error codes for better integration:
Before:
return {
status: 'ERROR',
message: 'Unauthorized'
};After:
// Errors are automatically wrapped with GraphQL error codes
return {
status: 'ERROR',
message: 'Unauthorized'
};
// Results in GraphQLError with proper error codeYou can now target specific sources with beforeSource and afterSource hooks:
{
"hooks": {
"beforeSource": {
"users": [
{
"composer": "./hooks/authHook.js",
"blocking": true
}
]
}
}
}Execute code after GraphQL execution:
{
"hooks": {
"afterAll": {
"composer": "./hooks/loggingHook.js",
"blocking": false
}
}
}Access persistent state in your hooks:
module.exports = {
myHook: async ({ context }) => {
const { state } = context;
// Store data
await state.put('user-session', 'session-data', { ttl: 3600 });
// Retrieve data
const session = await state.get('user-session');
// Delete data
await state.delete('user-session');
}
};If you're using TypeScript, you can now import types for better development experience:
import type {
HookFunction,
HookFunctionPayload,
HookResponse,
BeforeSourceHookFunctionPayload,
AfterSourceHookFunctionPayload,
AfterAllHookFunctionPayload
} from '@adobe/plugin-hooks';
const myHook: HookFunction = async (payload: HookFunctionPayload) => {
// Fully typed payload
};The package now includes comprehensive test coverage. You can run tests locally:
yarn test
yarn test --coverage- Source hooks are more efficient than global hooks as they only run for specific sources
- Non-blocking hooks don't affect response time
- State API provides persistent storage but has TTL limitations
- Memoization is automatically applied for better performance
- Node.js (v18 or later)
Run the linting script to check for errors:
yarn lintRun the test script to execute all tests:
yarn testFor test coverage include the --coverage flag:
yarn test --coverageRun the build script to compile the TypeScript code into ESM/CJS:
yarn buildThe build output will be in the dist directory.
Please refer to the contributing guidelines for more information.
This project is licensed under the Apache V2 License. See LICENSE for more information.