-
-
Notifications
You must be signed in to change notification settings - Fork 575
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert hooks system to a middleware system (#2071)
- Loading branch information
Showing
48 changed files
with
1,294 additions
and
604 deletions.
There are no files selected for viewing
This file contains 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,70 @@ | ||
--- | ||
"graphile-build": patch | ||
"postgraphile": patch | ||
"graphile-config": patch | ||
"@dataplan/pg": patch | ||
"grafserv": patch | ||
"grafast": patch | ||
--- | ||
|
||
`GrafastExecutionArgs` now accepts `resolvedPreset` and `requestContext` | ||
directly; passing these through additional arguments is now deprecated and | ||
support will be removed in a future revision. This affects: | ||
|
||
- `grafast()` | ||
- `execute()` | ||
- `subscribe()` | ||
- `hookArgs()` | ||
|
||
`graphile-config` has gained a middleware system which is more powerful than | ||
it's AsyncHooks system. Old hooks can be emulated through the middleware system | ||
safely since middleware is a superset of hooks' capabilities. `applyHooks` has | ||
been renamed to `orderedApply` (because it applies to more than just hooks), | ||
calling `applyHooks` will still work but is deprecated. | ||
|
||
🚨 `grafast` no longer automatically reads your `graphile.config.ts` or similar; | ||
you must do that yourself and pass the `resolvedPreset` to grafast via the | ||
`args`. This is to aid in bundling of grafast since it should not need to read | ||
from filesystem or dynamically load modules. | ||
|
||
`grafast` no longer outputs performance warning when you set | ||
`GRAPHILE_ENV=development`. | ||
|
||
🚨 `plugin.grafast.hooks.args` is now `plugin.grafast.middleware.prepareArgs`, | ||
and the signature has changed - you must be sure to call the `next()` function | ||
and ctx/resolvedPreset can be extracted directly from `args`: | ||
|
||
```diff | ||
const plugin = { | ||
grafast: { | ||
- hooks: { | ||
+ middleware: { | ||
- args({ args, ctx, resolvedPreset }) { | ||
+ prepareArgs(next, { args }) { | ||
+ const { requestContext: ctx, resolvedPreset } = args; | ||
// ... | ||
+ return next(); | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Many more middleware have been added; use TypeScript's autocomplete to see | ||
what's available until we have proper documentation for them. | ||
|
||
`plugin.grafserv.hooks.*` are still supported but deprecated; instead use | ||
middleware `plugin.grafserv.middleware.*` (note that call signatures have | ||
changed slightly, similar to the diff above): | ||
|
||
- `hooks.init` -> `middleware.setPreset` | ||
- `hooks.processGraphQLRequestBody` -> `middleware.processGraphQLRequestBody` | ||
- `hooks.ruruHTMLParts` -> `middleware.ruruHTMLParts` | ||
|
||
A few TypeScript types related to Hooks have been renamed, but their old names | ||
are still available, just deprecated. They will be removed in a future update: | ||
|
||
- `HookObject` -> `FunctionalityObject` | ||
- `PluginHook` -> `CallbackOrDescriptor` | ||
- `PluginHookObject` -> `CallbackDescriptor` | ||
- `PluginHookCallback` -> `UnwrapCallback` |
This file contains 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
This file contains 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 |
---|---|---|
@@ -1,65 +1,92 @@ | ||
import type { ExecutionArgs } from "graphql"; | ||
|
||
import { hook, NULL_PRESET } from "./config.js"; | ||
import type { | ||
GrafastExecutionArgs, | ||
PrepareArgsEvent, | ||
PromiseOrDirect, | ||
} from "./interfaces.js"; | ||
import { $$hooked } from "./interfaces.js"; | ||
import { getGrafastMiddleware } from "./middleware.js"; | ||
import { isPromiseLike } from "./utils.js"; | ||
const EMPTY_OBJECT: Record<string, never> = Object.freeze(Object.create(null)); | ||
|
||
/** @deprecated Pass `resolvedPreset` and `requestContext` via args directly */ | ||
export function hookArgs( | ||
rawArgs: ExecutionArgs, | ||
resolvedPreset: GraphileConfig.ResolvedPreset, | ||
ctx: Partial<Grafast.RequestContext>, | ||
): PromiseOrDirect<Grafast.ExecutionArgs>; | ||
export function hookArgs( | ||
rawArgs: GrafastExecutionArgs, | ||
): PromiseOrDirect<Grafast.ExecutionArgs>; | ||
/** | ||
* Applies Graphile Config hooks to your GraphQL request, e.g. to | ||
* populate context or similar. | ||
* | ||
* @experimental | ||
*/ | ||
export function hookArgs( | ||
rawArgs: ExecutionArgs, | ||
resolvedPreset: GraphileConfig.ResolvedPreset, | ||
ctx: Partial<Grafast.RequestContext>, | ||
): Grafast.ExecutionArgs | PromiseLike<Grafast.ExecutionArgs> { | ||
rawArgs: GrafastExecutionArgs, | ||
legacyResolvedPreset?: GraphileConfig.ResolvedPreset, | ||
legacyCtx?: Partial<Grafast.RequestContext>, | ||
): PromiseOrDirect<Grafast.ExecutionArgs> { | ||
if (legacyResolvedPreset !== undefined) { | ||
rawArgs.resolvedPreset = legacyResolvedPreset; | ||
} | ||
if (legacyCtx !== undefined) { | ||
rawArgs.requestContext = rawArgs.requestContext ?? legacyCtx; | ||
} | ||
const { | ||
middleware: rawMiddleware, | ||
resolvedPreset, | ||
contextValue: rawContextValue, | ||
} = rawArgs; | ||
// Make context mutable | ||
rawArgs.contextValue = Object.assign(Object.create(null), rawContextValue); | ||
const middleware = | ||
rawMiddleware === undefined && resolvedPreset != null | ||
? getGrafastMiddleware(resolvedPreset) | ||
: rawMiddleware ?? null; | ||
if (rawMiddleware === undefined) { | ||
rawArgs.middleware = middleware; | ||
} | ||
const args = rawArgs as Grafast.ExecutionArgs; | ||
// Assert that args haven't already been hooked | ||
if (args[$$hooked]) { | ||
throw new Error("Must not call hookArgs twice!"); | ||
} | ||
args[$$hooked] = true; | ||
|
||
// Make context mutable | ||
args.contextValue = Object.assign(Object.create(null), args.contextValue); | ||
if (middleware != null) { | ||
return middleware.run("prepareArgs", { args }, finalizeWithEvent); | ||
} else { | ||
return finalize(args); | ||
} | ||
} | ||
|
||
// finalize(args): args is deliberately shadowed | ||
const finalize = (args: Grafast.ExecutionArgs) => { | ||
const userContext = resolvedPreset.grafast?.context; | ||
if (typeof userContext === "function") { | ||
const result = userContext(ctx, args); | ||
if (isPromiseLike(result)) { | ||
// Deliberately shadowed 'result' | ||
return result.then((result) => { | ||
Object.assign(args.contextValue as Partial<Grafast.Context>, result); | ||
return args; | ||
}); | ||
} else { | ||
Object.assign(args.contextValue as Partial<Grafast.Context>, result); | ||
function finalize(args: Grafast.ExecutionArgs) { | ||
const userContext = args.resolvedPreset?.grafast?.context; | ||
const contextValue = args.contextValue as Partial<Grafast.Context>; | ||
if (typeof userContext === "function") { | ||
const result = userContext(args.requestContext ?? EMPTY_OBJECT, args); | ||
if (isPromiseLike(result)) { | ||
// Deliberately shadowed 'result' | ||
return result.then((result) => { | ||
Object.assign(contextValue, result); | ||
return args; | ||
} | ||
} else if (typeof userContext === "object" && userContext !== null) { | ||
Object.assign(args.contextValue as Partial<Grafast.Context>, userContext); | ||
return args; | ||
}); | ||
} else { | ||
Object.assign(contextValue, result); | ||
return args; | ||
} | ||
}; | ||
|
||
if ( | ||
resolvedPreset !== NULL_PRESET && | ||
resolvedPreset.plugins && | ||
resolvedPreset.plugins.length > 0 | ||
) { | ||
const event = { args, ctx, resolvedPreset }; | ||
const result = hook(resolvedPreset, "args", event); | ||
if (isPromiseLike(result)) { | ||
return result.then(() => finalize(event.args)); | ||
} else { | ||
return finalize(event.args); | ||
} | ||
} else if (typeof userContext === "object" && userContext !== null) { | ||
Object.assign(contextValue, userContext); | ||
return args; | ||
} else { | ||
return args; | ||
} | ||
return finalize(args); | ||
} | ||
|
||
function finalizeWithEvent(event: PrepareArgsEvent) { | ||
return finalize(event.args); | ||
} |
This file contains 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
This file contains 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 |
---|---|---|
@@ -1,133 +1,3 @@ | ||
import type { AsyncHooks, PluginHook } from "graphile-config"; | ||
|
||
export const NULL_PRESET: GraphileConfig.ResolvedPreset = Object.freeze( | ||
Object.create(null), | ||
); | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
type GraphileConfigModule = typeof import("graphile-config"); | ||
|
||
type PromiseOrValue<T> = T | Promise<T>; | ||
|
||
let graphileConfig: undefined | PromiseOrValue<null | GraphileConfigModule> = | ||
undefined; | ||
let graphileConfigLoaded = false; | ||
|
||
export function withGraphileConfig<T>( | ||
callback: (graphileConfig: GraphileConfigModule | null) => PromiseOrValue<T>, | ||
): PromiseOrValue<T> { | ||
if (graphileConfig === undefined) { | ||
// ESM: | ||
// This should be | ||
// graphileConfig = import("graphile-config").then( | ||
// but that causes a segfault in jest/node when testing third party | ||
// modules. So we had to convert everything to CommonJS. | ||
graphileConfig = new Promise<any>((resolve, reject) => { | ||
try { | ||
resolve(require("graphile-config")); | ||
} catch (e) { | ||
if (e.code === "ERR_REQUIRE_ESM") { | ||
return import("graphile-config").then(resolve, reject); | ||
} else { | ||
reject(e); | ||
} | ||
} | ||
}).then( | ||
(GC) => { | ||
graphileConfig = GC; | ||
graphileConfigLoaded = true; | ||
return GC; | ||
}, | ||
() => { | ||
graphileConfig = null; | ||
graphileConfigLoaded = true; | ||
return null; | ||
}, | ||
); | ||
} | ||
|
||
if (graphileConfigLoaded) { | ||
return callback(graphileConfig as GraphileConfigModule | null); | ||
} else { | ||
return (graphileConfig as Promise<any>).then(callback); | ||
} | ||
} | ||
|
||
const $$skipHooks = Symbol("skipHooks"); | ||
const $$hooksForPreset = Symbol("grafastHooks"); | ||
|
||
declare global { | ||
namespace GraphileConfig { | ||
interface ResolvedPreset { | ||
[$$hooksForPreset]?: null | AsyncHooks<GraphileConfig.GrafastHooks>; | ||
[$$skipHooks]?: Record<string, boolean>; | ||
} | ||
} | ||
} | ||
|
||
export function withHooks<TResult>( | ||
resolvedPreset: GraphileConfig.ResolvedPreset, | ||
callback: ( | ||
hooks: AsyncHooks<GraphileConfig.GrafastHooks> | null, | ||
) => PromiseOrValue<TResult>, | ||
) { | ||
const existing = resolvedPreset[$$hooksForPreset]; | ||
if (existing !== undefined) { | ||
return callback(existing); | ||
} | ||
if (!resolvedPreset.plugins || resolvedPreset.plugins.length === 0) { | ||
resolvedPreset[$$hooksForPreset] = null; | ||
return callback(null); | ||
} | ||
const plugins = resolvedPreset.plugins; | ||
return withGraphileConfig((gc) => { | ||
if (gc !== null) { | ||
const hooks = new gc.AsyncHooks<GraphileConfig.GrafastHooks>(); | ||
gc.applyHooks( | ||
plugins, | ||
(p) => p.grafast?.hooks, | ||
(name, fn, _plugin) => { | ||
hooks.hook(name, fn); | ||
}, | ||
); | ||
resolvedPreset[$$hooksForPreset] = hooks; | ||
return callback(hooks); | ||
} else { | ||
resolvedPreset[$$hooksForPreset] = null; | ||
return callback(null); | ||
} | ||
}); | ||
} | ||
|
||
export function hook<THookName extends keyof GraphileConfig.GrafastHooks>( | ||
resolvedPreset: GraphileConfig.ResolvedPreset, | ||
hookName: THookName, | ||
...args: Parameters< | ||
GraphileConfig.GrafastHooks[THookName] extends PluginHook<infer U> | ||
? U | ||
: never | ||
> | ||
): PromiseOrValue<void> { | ||
if (resolvedPreset[$$skipHooks]?.[hookName]) { | ||
return; | ||
} | ||
return withHooks(resolvedPreset, (hooks) => { | ||
if (hooks !== null) { | ||
if (hooks.callbacks[hookName] !== undefined) { | ||
return hooks.process(hookName, ...args); | ||
} else { | ||
if (!resolvedPreset[$$skipHooks]) { | ||
resolvedPreset[$$skipHooks] = Object.create(null); | ||
} | ||
resolvedPreset[$$skipHooks]![hookName] = true; | ||
return; | ||
} | ||
} else { | ||
if (!resolvedPreset[$$skipHooks]) { | ||
resolvedPreset[$$skipHooks] = Object.create(null); | ||
} | ||
resolvedPreset[$$skipHooks]![hookName] = true; | ||
return; | ||
} | ||
}); | ||
} |
Oops, something went wrong.