|
1 | 1 | import { |
2 | 2 | IMountableItem, IConstructorRouteOptions, IRouteOptions, |
3 | | - IOperationVariable, IResponse, |
| 3 | + IOperationVariableMap, IOperationVariable, IResponse, |
4 | 4 | } from '.'; |
5 | 5 |
|
6 | 6 | import { IncomingHttpHeaders } from 'http'; |
@@ -42,12 +42,40 @@ function cleanPath(path: string): string { |
42 | 42 | return `/${path}`; |
43 | 43 | } |
44 | 44 |
|
| 45 | + |
| 46 | +// NOTE: |
| 47 | +// Consider moving the introspection of the graphql query into the routes so that |
| 48 | +// we know for certain which variables are INPUT_VARIABLES and which are enums / strings. |
| 49 | +// |
| 50 | +// This attempts to typecast all unknown types to JSON but the try catch deoptimizes the parsing of the JSON |
| 51 | +// and may affect performance. |
| 52 | +function attemptJSONParse(variable: string): any { |
| 53 | + try { |
| 54 | + return JSON.parse(variable); |
| 55 | + } catch (e) { |
| 56 | + return variable; |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +function typecastVariable(variable: string, variableDefinition: IOperationVariable): any { |
| 61 | + switch (variableDefinition.type) { |
| 62 | + case 'Int': |
| 63 | + return parseInt(variable, 10); |
| 64 | + case 'Boolean': |
| 65 | + return Boolean(variable); |
| 66 | + case 'String': |
| 67 | + return variable; |
| 68 | + default: |
| 69 | + return attemptJSONParse(variable); |
| 70 | + } |
| 71 | +} |
| 72 | + |
45 | 73 | export default class Route implements IMountableItem { |
46 | 74 | public path!: string; |
47 | 75 | public httpMethod: string = 'get'; |
48 | 76 |
|
49 | 77 | public passThroughHeaders: string[] = []; |
50 | | - public operationVariables!: IOperationVariable[]; |
| 78 | + public operationVariables!: IOperationVariableMap; |
51 | 79 | public operationName!: string; |
52 | 80 |
|
53 | 81 | // TODO: |
@@ -128,16 +156,24 @@ export default class Route implements IMountableItem { |
128 | 156 | return this; |
129 | 157 | } |
130 | 158 |
|
131 | | - private getOperationVariables(operation: any): IOperationVariable[] { |
132 | | - return operation.variableDefinitions.map( |
133 | | - (node: any): IOperationVariable => ({ |
134 | | - name: node.variable.name.value, |
135 | | - required: node.type.kind === 'NonNullType', |
136 | | - type: translateVariableType(node), |
137 | | - array: isVariableArray(node), |
138 | | - defaultValue: (node.defaultValue || {}).value, |
139 | | - }) |
| 159 | + private getOperationVariables(operation: any): IOperationVariableMap { |
| 160 | + const variableMap: IOperationVariableMap = {}; |
| 161 | + |
| 162 | + operation.variableDefinitions.forEach( |
| 163 | + (node: any): void => { |
| 164 | + const variable: IOperationVariable = { |
| 165 | + name: node.variable.name.value, |
| 166 | + required: node.type.kind === 'NonNullType', |
| 167 | + type: translateVariableType(node), |
| 168 | + array: isVariableArray(node), |
| 169 | + defaultValue: (node.defaultValue || {}).value, |
| 170 | + }; |
| 171 | + |
| 172 | + variableMap[variable.name] = variable; |
| 173 | + } |
140 | 174 | ); |
| 175 | + |
| 176 | + return variableMap; |
141 | 177 | } |
142 | 178 |
|
143 | 179 | private setOperationName(operationName: string): void { |
@@ -189,7 +225,7 @@ export default class Route implements IMountableItem { |
189 | 225 | } |
190 | 226 |
|
191 | 227 | private get requiredVariables(): string[] { |
192 | | - return this.operationVariables |
| 228 | + return Object.values(this.operationVariables) |
193 | 229 | .filter( |
194 | 230 | ({ required, defaultValue }) => required && !defaultValue |
195 | 231 | ) |
@@ -227,11 +263,33 @@ export default class Route implements IMountableItem { |
227 | 263 | .filter(requiredVariable => !variablesAsKeys.includes(requiredVariable)); |
228 | 264 | } |
229 | 265 |
|
| 266 | + // When an encoded value is passed in, it is decoded automatically but will always |
| 267 | + // be a string. |
| 268 | + // |
| 269 | + // This method will iterate through all variables, check their definition type from the spec |
| 270 | + // and typecast them |
| 271 | + private typecastVariables(variables: { [key: string]: string }): { [key: string]: any } { |
| 272 | + const parsedVariables: { [key: string]: any } = {}; |
| 273 | + |
| 274 | + Object.entries(variables).forEach( |
| 275 | + ([variableName, value]) => { |
| 276 | + const variableDefinition = this.operationVariables[variableName]; |
| 277 | + |
| 278 | + parsedVariables[variableName] = typecastVariable(value, variableDefinition); |
| 279 | + } |
| 280 | + ); |
| 281 | + |
| 282 | + return parsedVariables; |
| 283 | + } |
| 284 | + |
230 | 285 | asExpressRoute() { |
231 | 286 | return async (req: express.Request, res: express.Response) => { |
232 | 287 | const { query, params, body } = req; |
233 | 288 |
|
234 | | - const providedVariables = { ...query, ...params, ...body }; |
| 289 | + const parsedQueryVariables = this.typecastVariables(query); |
| 290 | + const parsedPathVariables = this.typecastVariables(params); |
| 291 | + |
| 292 | + const providedVariables = { ...parsedQueryVariables, ...parsedPathVariables, ...body }; |
235 | 293 |
|
236 | 294 | // Assemble variables from query, path and default values |
237 | 295 | const assembledVariables = this.assembleVariables(providedVariables); |
@@ -309,15 +367,15 @@ export default class Route implements IMountableItem { |
309 | 367 |
|
310 | 368 | const pathVariableNames = matches.map(match => match.substr(1)); |
311 | 369 |
|
312 | | - return this.operationVariables.filter( |
| 370 | + return Object.values(this.operationVariables).filter( |
313 | 371 | ({ name }) => pathVariableNames.includes(name) |
314 | 372 | ); |
315 | 373 | } |
316 | 374 |
|
317 | 375 | get nonPathVariables(): IOperationVariable[] { |
318 | 376 | const pathVariableNames = this.pathVariables.map(({ name }) => name); |
319 | 377 |
|
320 | | - return this.operationVariables |
| 378 | + return Object.values(this.operationVariables) |
321 | 379 | .filter( |
322 | 380 | ({ name }) => !pathVariableNames.includes(name) |
323 | 381 | ); |
|
0 commit comments