|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +title: "Middleware" |
| 4 | +--- |
| 5 | + |
| 6 | +# Middleware |
| 7 | + |
| 8 | +## For plugin authors |
| 9 | + |
| 10 | +You should probably see the documentation for the relevant project that you |
| 11 | +want to add a middleware for. |
| 12 | + |
| 13 | +Generally middleware works by registering a handler function for a given action |
| 14 | +in a plugin, scoped within an entry named after the library (e.g. |
| 15 | +`plugin.libraryName.middleware.someAction`). |
| 16 | + |
| 17 | +Your handler function will be passed a `next` callback that calls the next |
| 18 | +handler, or performs the underlying action if there is no next handler, and any |
| 19 | +additional arguments. Normally there is exactly one additional argument, |
| 20 | +`event`, which describes the event that is being wrapped in middleware. |
| 21 | + |
| 22 | +Your function should: |
| 23 | + |
| 24 | +1. Take any actions that it needs to take before the underlying code is called |
| 25 | + (including mutating `event` if necessary) |
| 26 | +2. Then, either: |
| 27 | + 1. `return next()` (and you're done); or: |
| 28 | + 2. Call, await, and store the result of `next()`, then return a derivative |
| 29 | + thereof (after taking any necessary cleanup actions); or: |
| 30 | + 3. Use `return next.callback(cb)` where `cb` is a callback you define in |
| 31 | + traditional (non-promises!) Node.js style which receives the error (if |
| 32 | + any) as the first parameter, and the result (if no error) as the second, and |
| 33 | + must return the value to resolve as (or throw an error) |
| 34 | + |
| 35 | +:::warning Some middleware are synchronous! |
| 36 | + |
| 37 | +Some libraries enable middleware around synchronous methods, and expect the |
| 38 | +result of the middleware to be synchronous too. Internally, these libraries run |
| 39 | +this middleware with `middleware.runSync()` rather than `middleware.run()`; |
| 40 | +returning a promise from your middleware will throw an error in these cases. |
| 41 | + |
| 42 | +You should be able to tell a synchronous middleware from its TypeScript types: |
| 43 | +if the middleware's return type does not incorporate Promise or PromiseLike |
| 44 | +then it probably does not support promises. |
| 45 | + |
| 46 | +Unless you are certain a given middleware supports promises, you should not use |
| 47 | +`async`/`await` and should instead use `next.callback(...)` if you need to |
| 48 | +perform an action once the action is complete. |
| 49 | + |
| 50 | +::: |
| 51 | + |
| 52 | +Here's a simple example that just multiplies one of the parameters by two, and |
| 53 | +logs that this action has been called: |
| 54 | + |
| 55 | +```ts |
| 56 | +export const MySomeActionDoubleParameterPlugin: GraphileConfig.Plugin = { |
| 57 | + name: "MySomeActionDoubleParameterPlugin", |
| 58 | + libraryName: { |
| 59 | + middleware: { |
| 60 | + someAction(next, event) { |
| 61 | + console.log(`someAction(someParameter=${event.someParameter}) called`); |
| 62 | + event.someParameter = event.someParameter * 2; |
| 63 | + return next(); |
| 64 | + }, |
| 65 | + }, |
| 66 | + }, |
| 67 | +}; |
| 68 | +``` |
| 69 | + |
| 70 | +Here's an example that retries on error (but always forces promises): |
| 71 | + |
| 72 | +:::danger `next()` should be called exactly once unless otherwise stated |
| 73 | + |
| 74 | +In general libraries may assume that `next()` will be called once, so it might |
| 75 | +not be safe to retry `next()`. Libraries that support `next()` being called |
| 76 | +more than once should note this in the documentation for the relevant action. |
| 77 | + |
| 78 | +::: |
| 79 | + |
| 80 | +```ts |
| 81 | +export const MySomeActionRetryPlugin: GraphileConfig.Plugin = { |
| 82 | + name: "MySomeActionRetryPlugin", |
| 83 | + libraryName: { |
| 84 | + middleware: { |
| 85 | + async someAction(next, event) { |
| 86 | + let error!: Error; |
| 87 | + for (let attempts = 0; attempts < 3; attempts++) { |
| 88 | + if (attempts > 0) { |
| 89 | + // Wait a few milliseconds before trying again |
| 90 | + await sleep(attempts * 5); |
| 91 | + } |
| 92 | + try { |
| 93 | + return await next(); |
| 94 | + } catch (e) { |
| 95 | + error = e; |
| 96 | + } |
| 97 | + } |
| 98 | + throw error; |
| 99 | + }, |
| 100 | + }, |
| 101 | + }, |
| 102 | +}; |
| 103 | +``` |
| 104 | + |
| 105 | +Here's an example that does not introduce a promise unless one was already |
| 106 | +present (complex, right? Don't do this - see next example instead): |
| 107 | + |
| 108 | +```ts |
| 109 | +export const MySomeActionLogPlugin: GraphileConfig.Plugin = { |
| 110 | + name: "MySomeActionLogPlugin", |
| 111 | + libraryName: { |
| 112 | + middleware: { |
| 113 | + someAction(next, event) { |
| 114 | + console.log(`someAction(someParameter=${event.someParameter}) called`); |
| 115 | + // Optionally mutate event |
| 116 | + event.someParameter = event.someParameter * 2; |
| 117 | + |
| 118 | + const onSuccess = (result) => { |
| 119 | + console.log(`someAction() returned ${result}`); |
| 120 | + // Return `result` or a derivative thereof |
| 121 | + return result / 2; |
| 122 | + }; |
| 123 | + |
| 124 | + const onError = (error) => { |
| 125 | + console.error(`someAction() threw ${error}`); |
| 126 | + // Handle the error somehow... Or just rethrow it. |
| 127 | + throw error; |
| 128 | + }; |
| 129 | + |
| 130 | + const promiseOrValue = next(); |
| 131 | + try { |
| 132 | + if (isPromiseLike(promiseOrValue)) { |
| 133 | + return promiseOrValue.then(onSuccess, onError); |
| 134 | + } else { |
| 135 | + // Optionally perform any additional actions here |
| 136 | + return onSuccess(promiseOrValue); |
| 137 | + } |
| 138 | + } catch (e) { |
| 139 | + return onError(e); |
| 140 | + } |
| 141 | + }, |
| 142 | + }, |
| 143 | + }, |
| 144 | +}; |
| 145 | +``` |
| 146 | + |
| 147 | +Here's the above example again, but using `next.callback()` to handle the |
| 148 | +promises (if any) for us; this is the generally recommended approach: |
| 149 | + |
| 150 | +```ts |
| 151 | +export const MySomeActionLogPlugin: GraphileConfig.Plugin = { |
| 152 | + name: "MySomeActionLogPlugin", |
| 153 | + libraryName: { |
| 154 | + middleware: { |
| 155 | + someAction(next, event) { |
| 156 | + console.log(`someAction(someParameter=${event.someParameter}) called`); |
| 157 | + // Optionally mutate event |
| 158 | + event.someParameter = event.someParameter * 2; |
| 159 | + |
| 160 | + return next.callback((error, result) => { |
| 161 | + if (error) { |
| 162 | + console.error(`someAction() threw ${error}`); |
| 163 | + // Handle the error somehow... Or just rethrow it. |
| 164 | + throw error; |
| 165 | + } else { |
| 166 | + console.log(`someAction() returned ${result}`); |
| 167 | + // Return `result` or a derivative thereof |
| 168 | + return result / 2; |
| 169 | + } |
| 170 | + }); |
| 171 | + }, |
| 172 | + }, |
| 173 | + }, |
| 174 | +}; |
| 175 | +``` |
| 176 | + |
| 177 | +## For a library author |
| 178 | + |
| 179 | +(This section was primarily written by Benjie for Benjie... so you may want to |
| 180 | +skip it.) |
| 181 | + |
| 182 | +If you need to create a middleware system for your library, you might follow |
| 183 | +something along these lines (replacing `libraryName` with the name of your |
| 184 | +library): |
| 185 | + |
| 186 | +```ts |
| 187 | +/***** interfaces.ts *****/ |
| 188 | + |
| 189 | +// Define the middlewares that you support, their event type and their return type |
| 190 | +export interface MyMiddleware { |
| 191 | + someAction(event: SomeActionEvent): PromiseOrDirect<SomeActionResult>; |
| 192 | +} |
| 193 | +interface SomeActionEvent { |
| 194 | + someParameter: number; |
| 195 | + /* |
| 196 | + * Use a per-middleware-method interface to define the various pieces of |
| 197 | + * data relevant to this event. **ALWAYS** use the event as an abstraction |
| 198 | + * so that new information can be added in future without causing any |
| 199 | + * knock-on consequences. Note that these parameters of the event may be |
| 200 | + * mutated. The values here can be anything, they don't need to be simple |
| 201 | + * values. |
| 202 | + */ |
| 203 | +} |
| 204 | +// Middleware wraps a function call; this represents whatever the function returns |
| 205 | +type SomeActionResult = number; |
| 206 | + |
| 207 | +export type PromiseOrDirect<T> = Promise<T> | T; |
| 208 | + |
| 209 | +/***** getMiddleware.ts *****/ |
| 210 | + |
| 211 | +import { Middleware, orderedApply, resolvePresets } from "graphile-config"; |
| 212 | + |
| 213 | +export function getMiddleware(resolvedPreset: GraphileConfig.ResolvedPreset) { |
| 214 | + // Create your middleware instance. The generic describes the events supported |
| 215 | + const middleware = new Middleware<MyMiddleware>(); |
| 216 | + // Now apply the relevant middlewares registered by each plugin (if any) to the |
| 217 | + // Middleware instance |
| 218 | + orderedApply( |
| 219 | + resolvedPreset.plugins, |
| 220 | + (plugin) => plugin.libraryName?.middleware, |
| 221 | + (name, fn, _plugin) => { |
| 222 | + middleware.register(name, fn as any); |
| 223 | + }, |
| 224 | + ); |
| 225 | +} |
| 226 | + |
| 227 | +/***** main.ts *****/ |
| 228 | + |
| 229 | +// Get the user's Graphile Config from somewhere, e.g. |
| 230 | +import config from "./graphile.config.js"; |
| 231 | + |
| 232 | +// Resolve the above config, recursively applying all the presets it extends from |
| 233 | +const resolvedPreset = resolvePresets([config]); |
| 234 | + |
| 235 | +// Get the middleware for this preset |
| 236 | +const middleware = getMiddleware(resolvedPreset); |
| 237 | + |
| 238 | +// Then in the relevant place in your code, call the middleware around the |
| 239 | +// relevant functionality |
| 240 | +const result = await middleware.run( |
| 241 | + "someAction", |
| 242 | + { someParameter: 42 }, // < `event` object |
| 243 | + async (event) => { |
| 244 | + // Call the underlying method to perform the action. |
| 245 | + // Note: `event` will be the same object as above, but its contents may |
| 246 | + // have been modified by middlewares. |
| 247 | + return await someAction(event.someParameter); |
| 248 | + }, |
| 249 | +); |
| 250 | +// The value of `result` should match the return value of `someAction(...)` |
| 251 | +// (unless a middleware tweaked or replaced it, of course!) |
| 252 | + |
| 253 | +// This is the thing that your middleware wraps. It can do anything, it's just |
| 254 | +// an arbitrary JavaScript function. |
| 255 | +function someAction(someParameter: number): PromiseOrDirect<SomeActionResult> { |
| 256 | + // Do something here... |
| 257 | + if (Math.random() < 0.5) { |
| 258 | + return someParameter; |
| 259 | + } else { |
| 260 | + return sleep(200).then(() => someParameter); |
| 261 | + } |
| 262 | +} |
| 263 | +``` |
0 commit comments