Skip to content

Commit ce95157

Browse files
Introspect GraphQL endpoint for variable definitions
1 parent 2816a8a commit ce95157

File tree

4 files changed

+290
-77
lines changed

4 files changed

+290
-77
lines changed

OpenApi.ts

Lines changed: 193 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
11
import IMountableItem from './IMountableItem';
2+
import describeRouteVariables from './describeRouteVariables';
23
import Router from './Router';
34
import Route, { IOperationVariable } from './Route';
45
import express from 'express';
56

7+
interface IOpenApiOptions {
8+
title: string;
9+
version: string;
10+
termsOfService?: string;
11+
license?: string;
12+
basePath?: string;
13+
host?: string;
14+
}
15+
616
interface IParameter {
717
name: string;
818
required: boolean;
919
in: string;
10-
type: string;
20+
type?: string;
21+
schema?: IParameterArraySchema | IParameterItemTypeOrRef;
1122
default?: string | boolean | number;
1223
}
1324

25+
interface IParameterArraySchema {
26+
type: string;
27+
items: IParameterItemTypeOrRef;
28+
}
29+
30+
interface IParameterItemTypeOrRef {
31+
type?: string;
32+
$ref?: string;
33+
}
34+
1435
interface IBuildParametersArguments {
1536
variableDefinitions: IOperationVariable[];
1637
variableLocation: string;
@@ -22,24 +43,55 @@ function openApiPath(path: string): string {
2243
return path.replace(PATH_VARIABLES_REGEX, '{$1}');
2344
}
2445

46+
function resolveRefOrType(variableType: string): IParameterItemTypeOrRef {
47+
switch (variableType) {
48+
case 'String':
49+
return { type: 'string' };
50+
case 'Boolean':
51+
return { type: 'boolean' };
52+
case 'Int':
53+
return { type: 'number' };
54+
default:
55+
return { '$ref': `#/definitions/${variableType}` };
56+
}
57+
}
58+
2559
// TODO: Return Type and Attempt to get description from graphql
2660
function buildParametersArray({ variableDefinitions, variableLocation }: IBuildParametersArguments): IParameter[] {
2761
return variableDefinitions.map(
28-
(variableDefinition: IOperationVariable): IParameter => ({
29-
name: variableDefinition.name,
30-
required: variableDefinition.required,
31-
default: variableDefinition.defaultValue,
32-
in: variableLocation,
33-
type: variableDefinition.type,
34-
})
62+
(variableDefinition: IOperationVariable): IParameter => {
63+
const parameter: IParameter = {
64+
name: variableDefinition.name,
65+
required: variableDefinition.required,
66+
default: variableDefinition.defaultValue,
67+
in: variableLocation,
68+
};
69+
70+
if (variableDefinition.array) {
71+
parameter.schema = {
72+
type: 'array',
73+
items: resolveRefOrType(variableDefinition.type),
74+
};
75+
} else {
76+
const refOrType = resolveRefOrType(variableDefinition.type);
77+
78+
if (refOrType.type) {
79+
parameter.type = refOrType.type;
80+
} else {
81+
parameter.schema = refOrType;
82+
}
83+
}
84+
85+
return parameter;
86+
}
3587
);
3688
}
3789

3890
export class V2 implements IMountableItem {
3991
public path: string = '/docs/openapi/v2';
4092
public httpMethod: string = 'get';
4193

42-
constructor(options: any, private router?: Router) {
94+
constructor(private options: IOpenApiOptions, private router?: Router) {
4395
}
4496

4597
setRouter(router: Router): this {
@@ -54,7 +106,7 @@ export class V2 implements IMountableItem {
54106
return this;
55107
}
56108

57-
private generateDocumentation(): any {
109+
private async generateDocumentation(): Promise<any> {
58110
const { router } = this;
59111

60112
if (!router) {
@@ -65,78 +117,157 @@ export class V2 implements IMountableItem {
65117
`);
66118
}
67119

68-
const page: any = {
69-
swagger: '2.0',
70-
info: {},
71-
paths: {},
72-
};
120+
try {
121+
const {
122+
title,
123+
version,
124+
termsOfService,
125+
license,
126+
host,
127+
basePath,
128+
} = this.options;
73129

74-
router.routes.forEach((route) => {
75-
const { path, httpMethod } = route;
130+
const page: any = {
131+
swagger: '2.0',
132+
info: {},
133+
paths: {},
134+
produces: ['application/json'],
135+
definitions: {},
136+
};
76137

77-
const docPath = openApiPath(path);
138+
page.info.title = title;
139+
page.info.version = version;
78140

79-
if (!page.paths[docPath]) {
80-
page.paths[docPath] = {};
141+
if (termsOfService) {
142+
page.info.termsOfService = termsOfService;
81143
}
82144

83-
const routeDoc: any = page.paths[docPath][httpMethod] = {};
84-
85-
routeDoc.parameters = [];
86-
routeDoc.consumes = [];
87-
routeDoc.produces = ['application/json'];
88-
routeDoc.responses = {
89-
200: {
90-
description: 'Server alive. This does not mean that the query was completed succesfully. Check the errors object in the response',
91-
},
92-
400: {
93-
description: 'Authentication Required',
94-
},
95-
500: {
96-
description: 'Server error.',
97-
}
98-
};
145+
if (license) {
146+
page.info.license = { name: license };
147+
}
99148

100-
if (httpMethod === 'post') {
101-
routeDoc.consumes.push('application/json');
149+
if (host) {
150+
page.host = host;
102151
}
103152

104-
routeDoc.parameters.push(
105-
...buildParametersArray({
106-
variableDefinitions: route.queryVariables,
107-
variableLocation: 'query'
108-
})
109-
);
153+
if (basePath) {
154+
page.basePath = basePath;
155+
}
110156

111-
routeDoc.parameters.push(
112-
...buildParametersArray({
113-
variableDefinitions: route.pathVariables,
114-
variableLocation: 'path'
115-
})
116-
);
157+
router.routes.forEach((route) => {
158+
const { path, httpMethod } = route;
117159

118-
routeDoc.parameters.push(
119-
...buildParametersArray({
120-
variableDefinitions: route.bodyVariables,
121-
variableLocation: 'body'
122-
})
123-
);
124-
});
160+
const docPath = openApiPath(path);
125161

126-
return page;
162+
if (!page.paths[docPath]) {
163+
page.paths[docPath] = {};
164+
}
165+
166+
const routeDoc: any = page.paths[docPath][httpMethod] = {};
167+
168+
routeDoc.parameters = [];
169+
routeDoc.consumes = [];
170+
routeDoc.produces = ['application/json'];
171+
routeDoc.responses = {
172+
200: {
173+
description: 'Server alive. This does not mean that the query was completed succesfully. Check the errors object in the response',
174+
},
175+
400: {
176+
description: 'Authentication Required',
177+
},
178+
500: {
179+
description: 'Server error.',
180+
}
181+
};
182+
183+
if (httpMethod === 'post') {
184+
routeDoc.consumes.push('application/json');
185+
}
186+
187+
// TODO: 'push in parameters for header'
188+
189+
routeDoc.parameters.push(
190+
...buildParametersArray({
191+
variableDefinitions: route.queryVariables,
192+
variableLocation: 'query'
193+
})
194+
);
195+
196+
routeDoc.parameters.push(
197+
...buildParametersArray({
198+
variableDefinitions: route.pathVariables,
199+
variableLocation: 'path'
200+
})
201+
);
202+
203+
routeDoc.parameters.push(
204+
...buildParametersArray({
205+
variableDefinitions: route.bodyVariables,
206+
variableLocation: 'body'
207+
})
208+
);
209+
});
210+
211+
const introspectedVariableDefinitions: any = await describeRouteVariables(router);
212+
213+
Object.keys(introspectedVariableDefinitions).forEach((variableName) => {
214+
const variableDefinition: any = introspectedVariableDefinitions[variableName];
215+
216+
const definitionDoc: any = page.definitions[variableName] = {};
217+
218+
if (variableDefinition.kind === 'INPUT_OBJECT') {
219+
definitionDoc.type = 'object';
220+
definitionDoc.properties = {};
221+
222+
variableDefinition.inputFields.forEach((field: any) => {
223+
const inputDoc: any = definitionDoc.properties[field.name] = {};
224+
225+
inputDoc.type = 'string';
226+
inputDoc.description = field.type.description;
227+
228+
if (field.type.kind === 'ENUM') {
229+
inputDoc.enum = [];
230+
231+
field.type.enumValues.forEach((enumValue: any) => {
232+
inputDoc.enum.push(enumValue.name);
233+
});
234+
}
235+
});
236+
}
237+
238+
if (variableDefinition.kind === 'ENUM') {
239+
definitionDoc.type = 'string';
240+
definitionDoc.enum = [];
241+
242+
variableDefinition.enumValues.forEach((enumValue: any) => {
243+
definitionDoc.enum.push(enumValue.name);
244+
});
245+
}
246+
});
247+
248+
return page;
249+
} catch (error) {
250+
return {
251+
error: error.message,
252+
};
253+
}
127254
}
128255

129256
withOptions(options: {}): this {
130257
return this;
131258
}
132259

133260
asExpressRoute(): (req: express.Request, res: express.Response) => void {
134-
const doc = this.generateDocumentation();
261+
const generateDoc = this.generateDocumentation();
135262

136263
return (req: express.Request, res: express.Response) => {
137-
res
138-
.status(200)
139-
.json(doc);
264+
generateDoc.then(
265+
(doc) => {
266+
res
267+
.status(200)
268+
.json(doc);
269+
}
270+
);
140271
}
141272
}
142273

0 commit comments

Comments
 (0)