-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathendpoint.ts
More file actions
233 lines (215 loc) · 8.19 KB
/
endpoint.ts
File metadata and controls
233 lines (215 loc) · 8.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import { ResponseCache } from '../cache/response'
import { AdapterSettings } from '../config'
import { TransportRoutes } from '../transports'
import {
AdapterRequest,
AdapterRequestData,
Overrides,
makeLogger,
getCanonicalAdapterName,
canonicalizeAdapterNameKeys,
} from '../util'
import { InputParameters } from '../validation'
import { AdapterError } from '../validation/error'
import { TypeFromDefinition } from '../validation/input-params'
import {
AdapterDependencies,
AdapterEndpointInterface,
AdapterEndpointParams,
CustomInputValidator,
CustomOutputValidator,
EndpointGenerics,
EndpointRateLimitingConfig,
RequestTransform,
} from './types'
const logger = makeLogger('AdapterEndpoint')
export const DEFAULT_TRANSPORT_NAME = 'default_single_transport'
/**
* Main class to represent an endpoint within an External Adapter
*/
export class AdapterEndpoint<T extends EndpointGenerics> implements AdapterEndpointInterface<T> {
name: string
adapterName!: string
aliases?: string[] | undefined
transportRoutes: TransportRoutes<T>
inputParameters: InputParameters<T['Parameters']>
rateLimiting?: EndpointRateLimitingConfig | undefined
cacheKeyGenerator?: (data: TypeFromDefinition<T['Parameters']>) => string
customInputValidation?: CustomInputValidator<T>
customOutputValidation?: CustomOutputValidator | undefined
requestTransforms: RequestTransform<T>[]
overrides?: Record<string, string> | undefined
customRouter?: (
req: AdapterRequest<TypeFromDefinition<T['Parameters']>>,
settings: T['Settings'],
) => string
defaultTransport?: string
constructor(params: AdapterEndpointParams<T>) {
this.name = params.name
this.aliases = params.aliases
// These ifs are annoying but it's to make it type safe
if ('transportRoutes' in params) {
this.transportRoutes = params.transportRoutes
this.customRouter = params.customRouter
this.defaultTransport = params.defaultTransport
} else {
this.transportRoutes = new TransportRoutes<T>().register(
DEFAULT_TRANSPORT_NAME,
params.transport,
)
}
this.inputParameters = params.inputParameters || new InputParameters({})
this.rateLimiting = params.rateLimiting
this.cacheKeyGenerator = params.cacheKeyGenerator
this.customInputValidation = params.customInputValidation
this.customOutputValidation = params.customOutputValidation
this.overrides = params.overrides
this.requestTransforms = [
this.symbolOverrider.bind(this),
this.normalizeInputCase.bind(this),
...(params.requestTransforms || []),
]
}
/**
* Performs all necessary initialization processes that are async or need async initialized dependencies
*
* @param dependencies - all dependencies initialized at the adapter level
* @param adapterSettings - configuration for the adapter
*/
async initialize(
adapterName: string,
dependencies: AdapterDependencies,
adapterSettings: T['Settings'],
): Promise<void> {
this.adapterName = adapterName
const responseCache = new ResponseCache({
dependencies,
adapterSettings: adapterSettings as AdapterSettings,
adapterName,
endpointName: this.name,
inputParameters: this.inputParameters,
})
const transportDependencies = {
...dependencies,
responseCache,
}
logger.debug(`Initializing transports for endpoint "${this.name}"...`)
for (const [transportName, transport] of this.transportRoutes.entries()) {
await transport.initialize(transportDependencies, adapterSettings, this.name, transportName)
}
}
/**
* Takes the incoming request and applies all request transforms in the adapter
*
* @param req - the current adapter request
* @returns the request after passing through all request transforms
*/
runRequestTransforms(
req: AdapterRequest<TypeFromDefinition<T['Parameters']>>,
settings: T['Settings'],
): void {
for (const transform of this.requestTransforms) {
transform(req, settings)
}
}
getRequestOverrides(data: Record<string, string>, overrides?: Overrides) {
const overrideAdapterName = getCanonicalAdapterName(data['adapterNameOverride'])
const adapterName = getCanonicalAdapterName(this.adapterName)
const canonicalOverrides: Overrides | undefined = canonicalizeAdapterNameKeys(overrides)
return canonicalOverrides?.[overrideAdapterName] || canonicalOverrides?.[adapterName]
}
/**
* Default request transform that takes requests and manipulates base params
*
* @param adapter - the current adapter
* @param req - the current adapter request
* @returns the modified (or new) request
*/
symbolOverrider(req: AdapterRequest<TypeFromDefinition<T['Parameters']>>) {
const data = req.requestContext.data as Record<string, string>
const rawRequestBody = req.body as { data?: { overrides?: Overrides } }
const requestOverrides = this.getRequestOverrides(data, rawRequestBody.data?.overrides)
const base = data['base']
if (requestOverrides?.[base]) {
// Perform overrides specified in the request payload
data['base'] = requestOverrides[base]
} else if (this.overrides?.[base]) {
// Perform hardcoded adapter overrides
data['base'] = this.overrides[base]
}
return req
}
/**
* Default request transform that normalizes base and quote input parameters to uppercase.
* Controlled by the NORMALIZE_CASE_INPUTS setting (default: true).
* This prevents subscription churn when the same asset is requested with different casings.
*/
normalizeInputCase(
req: AdapterRequest<TypeFromDefinition<T['Parameters']>>,
settings: T['Settings'],
): void {
if (!(settings as Record<string, unknown>)['NORMALIZE_CASE_INPUTS']) {
return
}
const data = req.requestContext.data as Record<string, unknown>
if (typeof data['base'] === 'string') {
data['base'] = data['base'].toUpperCase()
}
if (typeof data['quote'] === 'string') {
data['quote'] = data['quote'].toUpperCase()
}
}
getTransportNameForRequest(
req: AdapterRequest<TypeFromDefinition<T['Parameters']>>,
settings: T['Settings'],
): string {
// If there's only one transport, return it
if (this.transportRoutes.get(DEFAULT_TRANSPORT_NAME)) {
return DEFAULT_TRANSPORT_NAME
}
// Attempt to get the transport to use from:
// 1. Custom router (whatever logic the user wrote)
// 2. Default router (try to get it from the input params)
// 3. Default transport (if it was specified in the instance params)
const rawTransportName =
(this.customRouter && this.customRouter(req, settings)) ||
this.defaultRouter(req) ||
this.defaultTransport
if (!rawTransportName) {
throw new AdapterError({
statusCode: 400,
message: `No result was fetched from a custom router, no transport was specified in the input parameters, and this endpoint does not have a default transport set.`,
})
}
const transportName = rawTransportName.toLowerCase()
if (!this.transportRoutes.get(transportName)) {
throw new AdapterError({
statusCode: 400,
message: `No transport found for key "${transportName}", must be one of ${JSON.stringify(
this.transportRoutes.routeNames(),
)}`,
})
}
logger.debug(`Request will be routed to transport "${transportName}"`)
return transportName
}
/**
* Default routing strategy. Will try and use the transport override if present
* or transport input parameter in the request body.
*
* @param req - The current adapter request
* @returns the transport param or override if present
*/
private defaultRouter(req: AdapterRequest<TypeFromDefinition<T['Parameters']>>) {
// DefaultRouter is called before customInputValidation, so we don't have
// the validation data on the requestContext yet.
const data: Record<string, string> = {}
const rawRequestBody = req.body as unknown as { data: AdapterRequestData }
const requestOverrides = this.getRequestOverrides(data, rawRequestBody.data?.overrides)
// Transport override
if (requestOverrides?.['transport']) {
return requestOverrides['transport']
}
return rawRequestBody.data?.transport
}
}