-
Notifications
You must be signed in to change notification settings - Fork 108
/
server.ts
311 lines (280 loc) · 11.3 KB
/
server.ts
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
'use strict';
import * as debug from 'debug';
import * as express from 'express';
import * as fs from 'fs-extra';
import * as _ from 'lodash';
import 'multer';
import * as path from 'path';
import * as YAML from 'yamljs';
import {
FileLimits, HttpMethod, ParameterConverter,
ServiceAuthenticator, ServiceFactory
} from './model/server-types';
import { ServerContainer } from './server-container';
const serverDebugger = debug('typescript-rest:server:build');
/**
* The Http server main class.
*/
export class Server {
/**
* Create the routes for all classes decorated with our decorators
*/
public static buildServices(router: express.Router, ...types: Array<any>) {
serverDebugger('Creating typescript-rest services handlers');
const serverContainer = ServerContainer.get();
serverContainer.router = router;
serverContainer.buildServices(types);
}
/**
* An alias for Server.loadServices()
*/
public static loadControllers(router: express.Router, patterns: string | Array<string>, baseDir?: string) {
Server.loadServices(router, patterns, baseDir);
}
/**
* Load all services from the files that matches the patterns provided
*/
public static loadServices(router: express.Router, patterns: string | Array<string>, baseDir?: string) {
serverDebugger('Loading typescript-rest services %j. BaseDir: %s', patterns, baseDir);
const importedTypes: Array<Function> = [];
const requireGlob = require('require-glob');
baseDir = baseDir || process.cwd();
const loadedModules: Array<any> = requireGlob.sync(patterns, {
cwd: baseDir
});
_.values(loadedModules).forEach(serviceModule => {
_.values(serviceModule)
.filter((service: Function) => typeof service === 'function')
.forEach((service: Function) => {
importedTypes.push(service);
});
});
try {
Server.buildServices(router, ...importedTypes);
} catch (e) {
serverDebugger('Error loading services for pattern: %j. Error: %o', patterns, e);
serverDebugger('ImportedTypes: %o', importedTypes);
throw new TypeError(`Error loading services for pattern: ${JSON.stringify(patterns)}. Error: ${e.message}`);
}
}
/**
* Return all paths accepted by the Server
*/
public static getPaths(): Array<string> {
const result = new Array<string>();
ServerContainer.get().getPaths().forEach(value => {
result.push(value);
});
return result;
}
/**
* Register a custom serviceFactory. It will be used to instantiate the service Objects
* If You plan to use a custom serviceFactory, You must ensure to call this method before any typescript-rest service declaration.
*/
public static registerServiceFactory(serviceFactory: ServiceFactory | string) {
let factory: ServiceFactory;
if (typeof serviceFactory === 'string') {
factory = require(serviceFactory);
} else {
factory = serviceFactory as ServiceFactory;
}
serverDebugger('Registering a new serviceFactory');
ServerContainer.get().serviceFactory = factory;
}
/**
* Register a service authenticator. It will be used to authenticate users before the service method
* invocations occurs.
*/
public static registerAuthenticator(authenticator: ServiceAuthenticator, name: string = 'default') {
serverDebugger('Registering a new authenticator with name %s', name);
ServerContainer.get().authenticator.set(name, authenticator);
}
/**
* Configure the Server to use [typescript-ioc](https://github.com/thiagobustamante/typescript-ioc)
* to instantiate the service objects.
* If You plan to use IoC, You must ensure to call this method before any typescript-rest service declaration.
* @param es6 if true, import typescript-ioc/es6
*/
public static useIoC(es6: boolean = false) {
const ioc = require(es6 ? 'typescript-ioc/es6' : 'typescript-ioc');
serverDebugger('Configuring a serviceFactory to use typescript-ioc to instantiate services. ES6: ', es6);
Server.registerServiceFactory({
create: (serviceClass) => {
return ioc.Container.get(serviceClass);
},
getTargetClass: (serviceClass: Function) => {
if (_.isArray(serviceClass)) {
return null;
}
let typeConstructor: any = serviceClass;
if (typeConstructor['name'] && typeConstructor['name'] !== 'ioc_wrapper') {
return typeConstructor as FunctionConstructor;
}
typeConstructor = typeConstructor['__parent'];
while (typeConstructor) {
if (typeConstructor['name'] && typeConstructor['name'] !== 'ioc_wrapper') {
return typeConstructor as FunctionConstructor;
}
typeConstructor = typeConstructor['__parent'];
}
serverDebugger('Can not identify the base Type for requested target: %o', serviceClass);
throw TypeError('Can not identify the base Type for requested target');
}
});
}
/**
* Return the set oh HTTP verbs configured for the given path
* @param servicePath The path to search HTTP verbs
*/
public static getHttpMethods(servicePath: string): Array<HttpMethod> {
const result = new Array<HttpMethod>();
ServerContainer.get().getHttpMethods(servicePath).forEach(value => {
result.push(value);
});
return result;
}
/**
* A string used for signing cookies. This is optional and if not specified,
* will not parse signed cookies.
* @param secret the secret used to sign
*/
public static setCookiesSecret(secret: string) {
serverDebugger('Setting a new secret for cookies: %s', secret);
ServerContainer.get().cookiesSecret = secret;
}
/**
* Specifies a function that will be used to decode a cookie's value.
* This function can be used to decode a previously-encoded cookie value
* into a JavaScript string.
* The default function is the global decodeURIComponent, which will decode
* any URL-encoded sequences into their byte representations.
*
* NOTE: if an error is thrown from this function, the original, non-decoded
* cookie value will be returned as the cookie's value.
* @param decoder The decoder function
*/
public static setCookiesDecoder(decoder: (val: string) => string) {
serverDebugger('Setting a new secret decoder');
ServerContainer.get().cookiesDecoder = decoder;
}
/**
* Set where to store the uploaded files
* @param dest Destination folder
*/
public static setFileDest(dest: string) {
serverDebugger('Setting a new destination for files: %s', dest);
ServerContainer.get().fileDest = dest;
}
/**
* Set a Function to control which files are accepted to upload
* @param filter The filter function
*/
public static setFileFilter(filter: (req: Express.Request, file: Express.Multer.File,
callback: (error: Error, acceptFile: boolean) => void) => void) {
serverDebugger('Setting a new filter for files');
ServerContainer.get().fileFilter = filter;
}
/**
* Set the limits of uploaded data
* @param limit The data limit
*/
public static setFileLimits(limit: FileLimits) {
serverDebugger('Setting a new fileLimits: %j', limit);
ServerContainer.get().fileLimits = limit;
}
/**
* Adds a converter for param values to have an ability to intercept the type that actually will be passed to service
* @param converter The converter
* @param type The target type that needs to be converted
*/
public static addParameterConverter(converter: ParameterConverter, type: Function): void {
serverDebugger('Adding a new parameter converter');
ServerContainer.get().paramConverters.set(type, converter);
}
/**
* Remove the converter associated with the given type.
* @param type The target type that needs to be converted
*/
public static removeParameterConverter(type: Function): void {
serverDebugger('Removing a parameter converter');
ServerContainer.get().paramConverters.delete(type);
}
/**
* Makes the server ignore next middlewares for all endpoints.
* It has the same effect than add @IgnoreNextMiddlewares to all
* services.
* @param value - true to ignore next middlewares.
*/
public static ignoreNextMiddlewares(value: boolean) {
serverDebugger('Ignoring next middlewares: %b', value);
ServerContainer.get().ignoreNextMiddlewares = value;
}
/**
* Creates and endpoint to publish the swagger documentation.
* @param router Express router
* @param options Options for swagger endpoint
*/
public static swagger(router: express.Router, options?: SwaggerOptions) {
const swaggerUi = require('swagger-ui-express');
options = Server.getOptions(options);
serverDebugger('Configuring open api documentation endpoints for options: %j', options);
const swaggerDocument: any = Server.loadSwaggerDocument(options);
if (options.host) {
swaggerDocument.host = options.host;
}
if (options.schemes) {
swaggerDocument.schemes = options.schemes;
}
router.get(path.posix.join('/', options.endpoint, 'json'), (req, res, next) => {
res.send(swaggerDocument);
});
router.get(path.posix.join('/', options.endpoint, 'yaml'), (req, res, next) => {
res.set('Content-Type', 'text/vnd.yaml');
res.send(YAML.stringify(swaggerDocument, 1000));
});
router.use(path.posix.join('/', options.endpoint), swaggerUi.serve, swaggerUi.setup(swaggerDocument, options.swaggerUiOptions));
}
private static loadSwaggerDocument(options: SwaggerOptions) {
let swaggerDocument: any;
if (_.endsWith(options.filePath, '.yml') || _.endsWith(options.filePath, '.yaml')) {
swaggerDocument = YAML.load(options.filePath);
}
else {
swaggerDocument = fs.readJSONSync(options.filePath);
}
serverDebugger('Loaded swagger configurations: %j', swaggerDocument);
return swaggerDocument;
}
private static getOptions(options: SwaggerOptions) {
options = _.defaults(options, {
endpoint: 'api-docs',
filePath: './swagger.json'
});
if (_.startsWith(options.filePath, '.')) {
options.filePath = path.join(process.cwd(), options.filePath);
}
return options;
}
}
export interface SwaggerOptions {
/**
* The path to a swagger file (json or yaml)
*/
filePath?: string;
/**
* Where to publish the docs
*/
endpoint?: string;
/**
* The hostname of the service
*/
host?: string;
/**
* The schemes used by the server
*/
schemes?: Array<string>;
/**
* Options to send to swagger-ui
*/
swaggerUiOptions?: object;
}