Skip to content

Commit 23a996a

Browse files
Initial OpenAPI module
1 parent a176e67 commit 23a996a

File tree

4 files changed

+285
-29
lines changed

4 files changed

+285
-29
lines changed

IMountableItem.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Router from './Router';
2+
import express from 'express';
3+
4+
export default interface IMountableItem {
5+
path: string;
6+
httpMethod: string;
7+
8+
at: (path: string) => this;
9+
10+
asExpressRoute: () => (req: express.Request, res: express.Response) => void;
11+
asKoaRoute: () => void;
12+
asMetal: () => void;
13+
14+
withOptions: (options: any) => this;
15+
setRouter: (router: Router) => this
16+
}
17+
18+
export interface IMountableItemClass {
19+
new (options: any, router?: Router): IMountableItem;
20+
}

OpenApi.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import IMountableItem from './IMountableItem';
2+
import Router from './Router';
3+
import Route, { IOperationVariable } from './Route';
4+
import express from 'express';
5+
6+
interface IParameter {
7+
name: string;
8+
required: boolean;
9+
in: string;
10+
type: string;
11+
default?: string | boolean | number;
12+
}
13+
14+
interface IBuildParametersArguments {
15+
variableDefinitions: IOperationVariable[];
16+
variableLocation: string;
17+
}
18+
19+
const PATH_VARIABLES_REGEX = /:([A-Za-z]+)/g
20+
21+
function openApiPath(path: string): string {
22+
return path.replace(PATH_VARIABLES_REGEX, '{$1}');
23+
}
24+
25+
// TODO: Return Type and Attempt to get description from graphql
26+
function buildParametersArray({ variableDefinitions, variableLocation }: IBuildParametersArguments): IParameter[] {
27+
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+
})
35+
);
36+
}
37+
38+
export class V2 implements IMountableItem {
39+
public path: string = '/docs/openapi/v2';
40+
public httpMethod: string = 'get';
41+
42+
constructor(options: any, private router?: Router) {
43+
}
44+
45+
setRouter(router: Router): this {
46+
this.router = router;
47+
48+
return this;
49+
}
50+
51+
at(path: string): this {
52+
this.path = path;
53+
54+
return this;
55+
}
56+
57+
private generateDocumentation(): any {
58+
const { router } = this;
59+
60+
if (!router) {
61+
throw new Error(`
62+
Router must be set in order to generate documentation. If you are using router.mount(), the router will automatically be set.
63+
If you are using this outside of a Router instance, please leverage #setRouter() to select a class that adheres to the router
64+
interface.
65+
`);
66+
}
67+
68+
const page: any = {
69+
swagger: '2.0',
70+
info: {},
71+
paths: {},
72+
};
73+
74+
router.routes.forEach((route) => {
75+
const { path, httpMethod } = route;
76+
77+
const docPath = openApiPath(path);
78+
79+
if (!page.paths[docPath]) {
80+
page.paths[docPath] = {};
81+
}
82+
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+
};
99+
100+
if (httpMethod === 'post') {
101+
routeDoc.consumes.push('application/json');
102+
}
103+
104+
routeDoc.parameters.push(
105+
...buildParametersArray({
106+
variableDefinitions: route.queryVariables,
107+
variableLocation: 'query'
108+
})
109+
);
110+
111+
routeDoc.parameters.push(
112+
...buildParametersArray({
113+
variableDefinitions: route.pathVariables,
114+
variableLocation: 'path'
115+
})
116+
);
117+
118+
routeDoc.parameters.push(
119+
...buildParametersArray({
120+
variableDefinitions: route.bodyVariables,
121+
variableLocation: 'body'
122+
})
123+
);
124+
});
125+
126+
return page;
127+
}
128+
129+
withOptions(options: {}): this {
130+
return this;
131+
}
132+
133+
asExpressRoute(): (req: express.Request, res: express.Response) => void {
134+
const doc = this.generateDocumentation();
135+
136+
return (req: express.Request, res: express.Response) => {
137+
res
138+
.status(200)
139+
.json(doc);
140+
}
141+
}
142+
143+
asKoaRoute() {
144+
throw new Error('not yet implemented');
145+
}
146+
147+
asMetal() {
148+
throw new Error('not yet implemented');
149+
}
150+
}

Route.ts

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import IMountableItem from './IMountableItem';
2+
import Router from './Router';
13
import { DocumentNode, parse, print, getOperationAST } from 'graphql';
24
import { AxiosTransformer, AxiosInstance, AxiosRequestConfig } from 'axios';
35
import * as express from 'express';
46

7+
const PATH_VARIABLES_REGEX = /:([A-Za-z]+)/g
8+
59
export interface IConstructorRouteOptions {
610
schema: DocumentNode | string; // GraphQL Document Type
711
operationName: string;
8-
12+
axios: AxiosInstance;
913
path?: string;
1014
cacheTimeInMs?: number;
1115
method?: string;
@@ -23,13 +27,14 @@ export interface IRouteOptions {
2327
defaultVariables?: {};
2428
}
2529

26-
interface IOperationVariable {
30+
export interface IOperationVariable {
2731
name: string;
2832
required: boolean;
33+
type: string;
2934
defaultValue?: string | boolean | number;
3035
}
3136

32-
interface IResponse {
37+
export interface IResponse {
3338
statusCode: number;
3439
body: {};
3540
}
@@ -42,6 +47,23 @@ enum EHTTPMethod {
4247
}
4348
*/
4449

50+
function translateVariableType(node: any): any {
51+
if (node.type.kind === 'NonNullType') {
52+
return translateVariableType(node.type);
53+
}
54+
55+
switch (node.type.name.value) {
56+
case 'Int':
57+
return 'number';
58+
case 'Boolean':
59+
return 'boolean';
60+
case 'String':
61+
default:
62+
return 'string';
63+
64+
}
65+
}
66+
4567
function cleanPath(path: string): string {
4668
if (path[0] === '/') {
4769
return path;
@@ -50,7 +72,7 @@ function cleanPath(path: string): string {
5072
return `/${path}`;
5173
}
5274

53-
export default class Route {
75+
export default class Route implements IMountableItem {
5476
public path!: string;
5577
public httpMethod: string = 'get';
5678

@@ -62,6 +84,7 @@ export default class Route {
6284
// the change
6385
private configurationIsFrozen: boolean = false;
6486

87+
private axios!: AxiosInstance;
6588
private schema!: DocumentNode;
6689

6790
private operationName!: string;
@@ -75,7 +98,7 @@ export default class Route {
7598

7699
private cacheTimeInMs: number = 0;
77100

78-
constructor(configuration: IConstructorRouteOptions, private axios: AxiosInstance) {
101+
constructor(configuration: IConstructorRouteOptions, private router?: Router) {
79102
this.configureRoute(configuration);
80103
}
81104

@@ -90,6 +113,7 @@ export default class Route {
90113
}
91114

92115
this.schema = typeof schema === 'string' ? parse(schema) : schema;
116+
this.axios = configuration.axios;
93117

94118
this.setOperationName(operationName);
95119

@@ -100,6 +124,12 @@ export default class Route {
100124
this.withOptions(options);
101125
}
102126

127+
setRouter(router: Router): this {
128+
this.router = router;
129+
130+
return this;
131+
}
132+
103133
at(path: string): this {
104134
this.path = cleanPath(path);
105135

@@ -118,6 +148,7 @@ export default class Route {
118148
(node: any): IOperationVariable => ({
119149
name: node.variable.name.value,
120150
required: node.type.kind === 'NonNullType',
151+
type: translateVariableType(node),
121152
defaultValue: node.defaultValue,
122153
})
123154
);
@@ -235,6 +266,10 @@ export default class Route {
235266
throw new Error('Not available! Submit PR');
236267
}
237268

269+
asMetal() {
270+
throw new Error('Not available! Submit PR');
271+
}
272+
238273
// areVariablesValid(variables: {}) {}
239274

240275
transformRequest(fn: AxiosTransformer): this {
@@ -255,6 +290,45 @@ export default class Route {
255290
return this;
256291
}
257292

293+
get queryVariables(): IOperationVariable[] {
294+
if (this.httpMethod === 'post') {
295+
return [];
296+
}
297+
298+
return this.nonPathVariables;
299+
}
300+
301+
get bodyVariables(): IOperationVariable[] {
302+
if (this.httpMethod === 'get') {
303+
return [];
304+
}
305+
306+
return this.nonPathVariables;
307+
}
308+
309+
get pathVariables(): IOperationVariable[] {
310+
const matches = this.path.match(PATH_VARIABLES_REGEX);
311+
312+
if (!matches) {
313+
return [];
314+
}
315+
316+
const pathVariableNames = matches.map(match => match.substr(1));
317+
318+
return this.operationVariables.filter(
319+
({ name }) => pathVariableNames.includes(name)
320+
);
321+
}
322+
323+
get nonPathVariables(): IOperationVariable[] {
324+
const pathVariableNames = this.pathVariables.map(({ name }) => name);
325+
326+
return this.operationVariables
327+
.filter(
328+
({ name }) => !pathVariableNames.includes(name)
329+
);
330+
}
331+
258332
private async makeRequest(variables: {}): Promise<IResponse> {
259333
const { axios, schema, operationName } = this;
260334

0 commit comments

Comments
 (0)