|
| 1 | +import { MaybePromise } from '@whatwg-node/promise-helpers'; |
| 2 | + |
| 3 | +export function withState< |
| 4 | + P extends { instrumentation?: GenericInstrumentation }, |
| 5 | + HttpState = object, |
| 6 | + GraphqlState = object, |
| 7 | + SubExecState = object, |
| 8 | +>( |
| 9 | + pluginFactory: ( |
| 10 | + getState: <SP extends {}>( |
| 11 | + payload: SP, |
| 12 | + ) => PayloadWithState<SP, HttpState, GraphqlState, SubExecState>['state'], |
| 13 | + ) => PluginWithState<P, HttpState, GraphqlState, SubExecState>, |
| 14 | +): P { |
| 15 | + const states: { |
| 16 | + forRequest?: WeakMap<Request, Partial<HttpState>>; |
| 17 | + forOperation?: WeakMap<object, Partial<GraphqlState>>; |
| 18 | + forSubgraphExecution?: WeakMap<{ context: any }, Partial<SubExecState>>; |
| 19 | + } = {}; |
| 20 | + |
| 21 | + function getProp(scope: keyof typeof states, key: any): PropertyDescriptor { |
| 22 | + return { |
| 23 | + get() { |
| 24 | + if (!states[scope]) states[scope] = new WeakMap<any, any>(); |
| 25 | + let value = states[scope].get(key as any); |
| 26 | + if (!value) states[scope].set(key, (value = {})); |
| 27 | + return value; |
| 28 | + }, |
| 29 | + enumerable: true, |
| 30 | + }; |
| 31 | + } |
| 32 | + |
| 33 | + function getState(payload: Payload) { |
| 34 | + if (!payload) { |
| 35 | + return undefined; |
| 36 | + } |
| 37 | + let { executionRequest, context, request } = payload; |
| 38 | + const state = {}; |
| 39 | + const defineState = (scope: keyof typeof states, key: any) => |
| 40 | + Object.defineProperty(state, scope, getProp(scope, key)); |
| 41 | + |
| 42 | + if (executionRequest) { |
| 43 | + defineState('forSubgraphExecution', executionRequest); |
| 44 | + // ExecutionRequest can happen outside of any Graphql Operation for Gateway internal usage like Introspection queries. |
| 45 | + // We check for `params` to be present, which means it's actually a GraphQL context. |
| 46 | + if (executionRequest.context?.params) context = executionRequest.context; |
| 47 | + } |
| 48 | + if (context) { |
| 49 | + defineState('forOperation', context); |
| 50 | + if (context.request) request = context.request; |
| 51 | + } |
| 52 | + if (request) { |
| 53 | + defineState('forRequest', request); |
| 54 | + } |
| 55 | + return state; |
| 56 | + } |
| 57 | + |
| 58 | + function addStateGetters(src: any) { |
| 59 | + const result: any = {}; |
| 60 | + for (const [hookName, hook] of Object.entries(src) as any) { |
| 61 | + if (typeof hook !== 'function') { |
| 62 | + result[hookName] = hook; |
| 63 | + } else { |
| 64 | + result[hookName] = { |
| 65 | + [hook.name](payload: any, ...args: any[]) { |
| 66 | + if (payload && (payload.request || payload.context || payload.executionRequest)) { |
| 67 | + return hook( |
| 68 | + { |
| 69 | + ...payload, |
| 70 | + get state() { |
| 71 | + return getState(payload); |
| 72 | + }, |
| 73 | + }, |
| 74 | + ...args, |
| 75 | + ); |
| 76 | + } else { |
| 77 | + return hook(payload, ...args); |
| 78 | + } |
| 79 | + }, |
| 80 | + }[hook.name]; |
| 81 | + } |
| 82 | + } |
| 83 | + return result; |
| 84 | + } |
| 85 | + |
| 86 | + const { instrumentation, ...hooks } = pluginFactory(getState as any); |
| 87 | + |
| 88 | + const pluginWithState = addStateGetters(hooks); |
| 89 | + if (instrumentation) { |
| 90 | + pluginWithState.instrumentation = addStateGetters(instrumentation); |
| 91 | + } |
| 92 | + |
| 93 | + return pluginWithState as P; |
| 94 | +} |
| 95 | + |
| 96 | +export type HttpState<T> = { |
| 97 | + forRequest: Partial<T>; |
| 98 | +}; |
| 99 | + |
| 100 | +export type GraphQLState<T> = { |
| 101 | + forOperation: Partial<T>; |
| 102 | +}; |
| 103 | + |
| 104 | +export type GatewayState<T> = { |
| 105 | + forSubgraphExecution: Partial<T>; |
| 106 | +}; |
| 107 | + |
| 108 | +export function getMostSpecificState<T>( |
| 109 | + state: Partial<HttpState<T> & GraphQLState<T> & GatewayState<T>> = {}, |
| 110 | +): Partial<T> | undefined { |
| 111 | + const { forOperation, forRequest, forSubgraphExecution } = state; |
| 112 | + return forSubgraphExecution ?? forOperation ?? forRequest; |
| 113 | +} |
| 114 | + |
| 115 | +type Payload = { |
| 116 | + request?: Request; |
| 117 | + context?: any; |
| 118 | + executionRequest?: { context: any }; |
| 119 | +}; |
| 120 | + |
| 121 | +type GenericInstrumentation = Record< |
| 122 | + string, |
| 123 | + (payload: any, wrapped: () => MaybePromise<void>) => MaybePromise<void> |
| 124 | +>; |
| 125 | + |
| 126 | +// Brace yourself! TS Wizardry is coming! |
| 127 | + |
| 128 | +type PayloadWithState<T, Http, GraphQL, Gateway> = T extends { |
| 129 | + executionRequest: any; |
| 130 | +} |
| 131 | + ? T & { |
| 132 | + state: Partial<HttpState<Http> & GraphQLState<GraphQL>> & GatewayState<Gateway>; |
| 133 | + } |
| 134 | + : T extends { |
| 135 | + executionRequest?: any; |
| 136 | + } |
| 137 | + ? T & { |
| 138 | + state: Partial<HttpState<Http> & GraphQLState<GraphQL> & GatewayState<Gateway>>; |
| 139 | + } |
| 140 | + : T extends { context: any } |
| 141 | + ? T & { state: HttpState<Http> & GraphQLState<GraphQL> } |
| 142 | + : T extends { request: any } |
| 143 | + ? T & { state: HttpState<Http> } |
| 144 | + : T extends { request?: any } |
| 145 | + ? T & { state: Partial<HttpState<Http>> } |
| 146 | + : T; |
| 147 | + |
| 148 | +export type PluginWithState<P, Http = object, GraphQL = object, Gateway = object> = { |
| 149 | + [K in keyof P]: K extends 'instrumentation' |
| 150 | + ? P[K] extends infer Instrumentation | undefined |
| 151 | + ? { |
| 152 | + [I in keyof Instrumentation]: Instrumentation[I] extends |
| 153 | + | ((payload: infer IP, ...args: infer Args) => infer IR) |
| 154 | + | undefined |
| 155 | + ? |
| 156 | + | ((payload: PayloadWithState<IP, Http, GraphQL, Gateway>, ...args: Args) => IR) |
| 157 | + | undefined |
| 158 | + : Instrumentation[I]; |
| 159 | + } |
| 160 | + : P[K] |
| 161 | + : P[K] extends ((payload: infer T) => infer R) | undefined |
| 162 | + ? ((payload: PayloadWithState<T, Http, GraphQL, Gateway>) => R) | undefined |
| 163 | + : P[K]; |
| 164 | +}; |
0 commit comments