Skip to content

Commit bc03aec

Browse files
authored
Merge branch 'main' into at-get
2 parents a0b9cce + db05daa commit bc03aec

File tree

1 file changed

+263
-0
lines changed

1 file changed

+263
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

Comments
 (0)