From 9846cd7e6b748f7c5cc176626dfc5cf47ec62e40 Mon Sep 17 00:00:00 2001 From: Kristopher Maschi Date: Tue, 29 Jul 2025 12:48:48 -0400 Subject: [PATCH 1/5] feat(context-state): CEXT-4829 - Extracted hook function import logic for reuse. - Added secrets, state, and logger to context in all hooks(beforeAll, afterAll, beforeSource, afterSource). - Resolved linting/style warnings. --- src/afterAllExecutor.ts | 9 ++- src/beforeAllExecutor.ts | 8 ++- src/index.ts | 23 +++++++- src/types.ts | 27 +++++++++ src/utils/hookResolver.ts | 113 ++++++++++++++++++-------------------- 5 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/afterAllExecutor.ts b/src/afterAllExecutor.ts index c2f5949..4dccf8a 100644 --- a/src/afterAllExecutor.ts +++ b/src/afterAllExecutor.ts @@ -18,6 +18,7 @@ import type { GraphQLData, GraphQLError as GraphQLErrorType, GraphQLResult, + StateApi, } from './types'; import type { YogaLogger, GraphQLParams } from 'graphql-yoga'; import { PLUGIN_HOOKS_ERROR_CODES } from './errorCodes'; @@ -28,10 +29,11 @@ export interface AfterAllExecutionContext { body: unknown; headers: Record; secrets: Record; + state: StateApi; + logger: YogaLogger; document: unknown; result: { data?: GraphQLData; errors?: GraphQLErrorType[] }; setResultAndStopExecution: (result: GraphQLResult) => void; - logger: YogaLogger; afterAll: HookConfig; } @@ -45,17 +47,18 @@ export async function executeAfterAllHook( body, headers, secrets, + state, + logger, document, result, setResultAndStopExecution, - logger, afterAll, } = context; try { // Create payload with the execution result const payload = { - context: { params, request, body, headers, secrets }, + context: { params, request, body, headers, secrets, state, logger }, document, result, // This is the GraphQL execution result }; diff --git a/src/beforeAllExecutor.ts b/src/beforeAllExecutor.ts index 3454417..b6c699e 100644 --- a/src/beforeAllExecutor.ts +++ b/src/beforeAllExecutor.ts @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import { GraphQLError } from 'graphql/error'; import getBeforeAllHookHandler, { UpdateContextFn } from './handleBeforeAllHooks'; -import type { HookConfig, MemoizedFns, GraphQLResult } from './types'; +import type { HookConfig, MemoizedFns, GraphQLResult, StateApi } from './types'; import type { YogaLogger, GraphQLParams } from 'graphql-yoga'; import { PLUGIN_HOOKS_ERROR_CODES } from './errorCodes'; @@ -22,6 +22,8 @@ export interface BeforeAllExecutionContext { body: unknown; headers: Record; secrets: Record; + state: StateApi; + logger: YogaLogger; document: unknown; updateContext: UpdateContextFn; setResultAndStopExecution: (result: GraphQLResult) => void; @@ -37,6 +39,8 @@ export async function executeBeforeAllHook( body, headers, secrets, + state, + logger, document, updateContext, setResultAndStopExecution, @@ -44,7 +48,7 @@ export async function executeBeforeAllHook( try { const payload = { - context: { params, request, body, headers, secrets }, + context: { params, request, body, headers, secrets, state, logger }, document, }; await beforeAllHookHandler({ payload, updateContext }); diff --git a/src/index.ts b/src/index.ts index 5f26838..0a052e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import type { GraphQLData, GraphQLError as GraphQLErrorType, SourceHookConfig, + StateApi, } from './types'; import getBeforeSourceHookHandler from './handleBeforeSourceHooks'; import type { YogaLogger, Plugin, YogaInitialContext } from 'graphql-yoga'; @@ -80,6 +81,9 @@ export default async function hooksPlugin(config: PluginConfig): Promise = {}; + // Check if any hooks are configured const hasAnyHooks = beforeAll || afterAll || beforeSource || afterSource; if (!hasAnyHooks) { @@ -88,6 +92,7 @@ export default async function hooksPlugin(config: PluginConfig): Promise {}, }; } + const memoizedFns: MemoizedFns = { afterSource: {}, beforeSource: {}, @@ -105,6 +110,9 @@ export default async function hooksPlugin(config: PluginConfig): Promise; + const state = ('state' in context ? context.state : {}) as StateApi; + serverContext.secrets = secrets; + serverContext.state = state; let body = {}; if (request && request.body) { body = request.body; @@ -143,6 +151,8 @@ export default async function hooksPlugin(config: PluginConfig): Promise; + + /** + * 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; + + /** + * Delete a key-value pair. + * @param key + */ + delete(key: string): Promise; +} + export interface UserContext extends YogaInitialContext { headers?: Record; secrets?: Record; + state?: StateApi; } export type SourceHookConfig = Record; @@ -72,6 +98,7 @@ export interface PayloadContext { body: unknown; headers?: Record; secrets?: Record; + state?: StateApi; } export type HookFunction = ( diff --git a/src/utils/hookResolver.ts b/src/utils/hookResolver.ts index 2d8cead..c0cd3ec 100644 --- a/src/utils/hookResolver.ts +++ b/src/utils/hookResolver.ts @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import type { YogaLogger } from 'graphql-yoga'; import type { OperationDefinitionNode } from 'graphql'; -import type { HookConfig, HookFunction, MemoizedFns } from '../types'; +import type { HookConfig, HookFunction, MemoizedFns, StateApi } from '../types'; //@ts-expect-error The dynamic import is a workaround for cjs import importFn from '../dynamicImport'; import { @@ -34,12 +34,22 @@ export interface HookResolverConfig { memoizedFns: MemoizedFns; } export interface BeforeSourceHookPayload { + context: { + logger: YogaLogger; + secrets: Record; + state: StateApi; + }; sourceName: string; request: RequestInit; operation: OperationDefinitionNode; } export interface AfterSourceHookPayload { + context: { + logger: YogaLogger; + secrets: Record; + state: StateApi; + }; sourceName: string; request: RequestInit; operation: OperationDefinitionNode; @@ -67,34 +77,11 @@ export interface SourceHookExecConfig { sourceName: string; } -/** - * Resolves and memoizes hook functions with consistent logic for both beforeAll and afterAll hooks - * - * @param config - Configuration object containing hook config, type, and dependencies - * @returns Promise - The resolved hook function or undefined if none configured - * - * @example - * ```typescript - * const hookFn = await resolveHookFunction({ - * hookConfig: beforeAllConfig, - * hookType: 'beforeAll', - * baseDir: '/path/to/base', - * logger: yogaLogger, - * memoizedFns: memoizedFunctions - * }); - * ``` - */ -export async function resolveHookFunction( - config: HookResolverConfig, -): Promise { - const { hookConfig, hookType, baseDir, logger, memoizedFns } = config; - - // Check if function is already memoized - const memoizedFn = memoizedFns[hookType]; - if (memoizedFn) { - return memoizedFn; - } - +async function getHookFunction( + config: HookResolverConfig | SourceHookResolverConfig, + hookConfig: HookConfig, +) { + const { baseDir, logger } = config; let hookFunction: HookFunction | undefined; // Resolve function based on configuration type @@ -127,6 +114,39 @@ export async function resolveHookFunction( }); } + return hookFunction; +} + +/** + * Resolves and memoizes hook functions with consistent logic for both beforeAll and afterAll hooks + * + * @param config - Configuration object containing hook config, type, and dependencies + * @returns Promise - The resolved hook function or undefined if none configured + * + * @example + * ```typescript + * const hookFn = await resolveHookFunction({ + * hookConfig: beforeAllConfig, + * hookType: 'beforeAll', + * baseDir: '/path/to/base', + * logger: yogaLogger, + * memoizedFns: memoizedFunctions + * }); + * ``` + */ +export async function resolveHookFunction( + config: HookResolverConfig, +): Promise { + const { hookConfig, hookType, memoizedFns } = config; + + // Check if function is already memoized + const memoizedFn = memoizedFns[hookType]; + if (memoizedFn) { + return memoizedFn; + } + + const hookFunction: HookFunction | undefined = await getHookFunction(config, hookConfig); + // Memoize the resolved function if (hookFunction) { memoizedFns[hookType] = hookFunction; @@ -141,6 +161,7 @@ export async function resolveHookFunction( * @param hookConfig - The hook configuration * @param index - The index of the hook in the array * @param config - Configuration object containing dependencies + * @param sourceName - The name of the source for which the hook is being resolved * @returns Promise - The resolved hook function or undefined */ export async function resolveSourceHookFunction( @@ -149,7 +170,7 @@ export async function resolveSourceHookFunction( config: SourceHookResolverConfig, sourceName: string, ): Promise { - const { hookType, baseDir, logger, memoizedFns } = config; + const { hookType, memoizedFns } = config; // Check if function is already memoized if ( @@ -160,37 +181,7 @@ export async function resolveSourceHookFunction( return memoizedFns[hookType][sourceName][index] as HookFunction; } - let hookFunction: HookFunction | undefined; - - // Resolve function based on configuration type - if (isRemoteFn(hookConfig.composer || '')) { - // Remote endpoint function - logger.debug('Invoking remote function %s', hookConfig.composer); - hookFunction = await getWrappedRemoteHookFunction(hookConfig.composer!, { - baseDir, - importFn, - logger, - blocking: hookConfig.blocking, - }); - } else if (isModuleFn(hookConfig)) { - // Module function (bundled scenarios) - logger.debug('Invoking local module function %s %s', hookConfig.module, hookConfig.fn); - hookFunction = await getWrappedLocalModuleHookFunction(hookConfig.module!, hookConfig.fn!, { - baseDir, - importFn, - logger, - blocking: hookConfig.blocking, - }); - } else { - // Local function at runtime - logger.debug('Invoking local function %s', hookConfig.composer); - hookFunction = await getWrappedLocalHookFunction(hookConfig.composer!, { - baseDir, - importFn, - logger, - blocking: hookConfig.blocking, - }); - } + const hookFunction: HookFunction | undefined = await getHookFunction(config, hookConfig); // Memoize the resolved function if (hookFunction) { From d9c20ebeee6abc287b85833f52c3f92614d69df5 Mon Sep 17 00:00:00 2001 From: Kristopher Maschi Date: Tue, 29 Jul 2025 12:49:25 -0400 Subject: [PATCH 2/5] feat(context-state): CEXT-4829 - Extracted hook function import logic for reuse. - Added secrets, state, and logger to context in all hooks(beforeAll, afterAll, beforeSource, afterSource). - Resolved linting/style warnings. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0646ea7..c7955bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/plugin-hooks", - "version": "0.3.5-alpha.4", + "version": "0.3.5", "publishConfig": { "access": "public" }, From b981e612d717ec5af21a89a944c0da8b881e717c Mon Sep 17 00:00:00 2001 From: Kristopher Maschi Date: Wed, 30 Jul 2025 14:15:42 -0400 Subject: [PATCH 3/5] feat(context-state): CEXT-4829 - Added extensions to beforeSource/afterSource errors. --- src/errorCodes.ts | 6 ++++ src/index.ts | 91 +++++++++++++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/errorCodes.ts b/src/errorCodes.ts index 32669db..e5b6731 100644 --- a/src/errorCodes.ts +++ b/src/errorCodes.ts @@ -18,6 +18,12 @@ export const PLUGIN_HOOKS_ERROR_CODES = { /** Error during beforeAll hook execution */ ERROR_PLUGIN_HOOKS_BEFORE_ALL: 'ERROR_PLUGIN_HOOKS_BEFORE_ALL', + /** Error during beforeSource hook execution */ + ERROR_PLUGIN_HOOKS_BEFORE_SOURCE: 'ERROR_PLUGIN_HOOKS_BEFORE_SOURCE', + + /** Error during afterSource hook execution */ + ERROR_PLUGIN_HOOKS_AFTER_SOURCE: 'ERROR_PLUGIN_HOOKS_AFTER_SOURCE', + /** Error during afterAll hook execution */ ERROR_PLUGIN_HOOKS_AFTER_ALL: 'ERROR_PLUGIN_HOOKS_AFTER_ALL', } as const; diff --git a/src/index.ts b/src/index.ts index 0a052e2..0e9ef60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,17 +10,18 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +import { GraphQLError } from 'graphql/error'; import { UpdateContextFn } from './handleBeforeAllHooks'; import { createBeforeAllHookHandler, executeBeforeAllHook } from './beforeAllExecutor'; import { createAfterAllHookHandler, executeAfterAllHook } from './afterAllExecutor'; -import type { +import { HookConfig, MemoizedFns, UserContext, GraphQLData, GraphQLError as GraphQLErrorType, SourceHookConfig, - StateApi, + StateApi, PLUGIN_HOOKS_ERROR_CODES, } from './types'; import getBeforeSourceHookHandler from './handleBeforeSourceHooks'; import type { YogaLogger, Plugin, YogaInitialContext } from 'graphql-yoga'; @@ -210,22 +211,33 @@ export default async function hooksPlugin(config: PluginConfig): Promise Date: Wed, 30 Jul 2025 17:16:23 -0400 Subject: [PATCH 4/5] feat(context-state): CEXT-4829 - Fixed linting errors. --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 0e9ef60..2c582bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,8 @@ import { GraphQLData, GraphQLError as GraphQLErrorType, SourceHookConfig, - StateApi, PLUGIN_HOOKS_ERROR_CODES, + StateApi, + PLUGIN_HOOKS_ERROR_CODES, } from './types'; import getBeforeSourceHookHandler from './handleBeforeSourceHooks'; import type { YogaLogger, Plugin, YogaInitialContext } from 'graphql-yoga'; From ac22d048a3392e218ba1708d17451d1bd031aa5e Mon Sep 17 00:00:00 2001 From: Kristopher Maschi Date: Wed, 30 Jul 2025 17:20:29 -0400 Subject: [PATCH 5/5] feat(context-state): CEXT-4829 - Updated package version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7955bf..6903106 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/plugin-hooks", - "version": "0.3.5", + "version": "0.3.6", "publishConfig": { "access": "public" },