typescript-rest
Version:
A Library to create RESTFul APIs with Typescript
265 lines (240 loc) • 10.4 kB
text/typescript
'use strict';
import * as debug from 'debug';
import * as express from 'express';
import * as _ from 'lodash';
import { Errors } from '../typescript-rest';
import { ServiceClass, ServiceMethod, ServiceProperty } from './model/metadata';
import { DownloadBinaryData, DownloadResource, NoResponse } from './model/return-types';
import { HttpMethod, ReferencedResource, ServiceContext, ServiceProcessor } from './model/server-types';
import { ParameterProcessor } from './parameter-processor';
import { ServerContainer } from './server-container';
export class ServiceInvoker {
private serviceClass: ServiceClass;
private serviceMethod: ServiceMethod;
private preProcessors: Array<ServiceProcessor>;
private postProcessors: Array<ServiceProcessor>;
private debugger = debug('typescript-rest:service-invoker:runtime');
constructor(serviceClass: ServiceClass, serviceMethod: ServiceMethod) {
this.serviceClass = serviceClass;
this.serviceMethod = serviceMethod;
this.preProcessors = _.union(serviceMethod.preProcessors, serviceClass.preProcessors);
this.postProcessors = _.union(serviceMethod.postProcessors, serviceClass.postProcessors);
}
public async callService(context: ServiceContext) {
try {
await this.callTargetEndPoint(context);
if (this.mustCallNext()) {
context.next();
} else if (this.debugger.enabled) {
this.debugger('Ignoring next middlewares');
}
}
catch (err) {
context.next(err);
}
}
private mustCallNext() {
return !ServerContainer.get().ignoreNextMiddlewares &&
!this.serviceMethod.ignoreNextMiddlewares && !this.serviceClass.ignoreNextMiddlewares;
}
private async runPreProcessors(context: ServiceContext): Promise<void> {
this.debugger('Running preprocessors');
for (const processor of this.preProcessors) {
await Promise.resolve(processor(context.request, context.response));
}
}
private async runPostProcessors(context: ServiceContext): Promise<void> {
this.debugger('Running postprocessors');
for (const processor of this.postProcessors) {
await Promise.resolve(processor(context.request, context.response));
}
}
private async callTargetEndPoint(context: ServiceContext) {
this.debugger('Calling targetEndpoint %s', this.serviceMethod.resolvedPath);
this.checkAcceptance(context);
if (this.preProcessors.length) {
await this.runPreProcessors(context);
}
const serviceObject = this.createService(context);
const args = this.buildArgumentsList(context);
const toCall = this.getMethodToCall();
if (this.debugger.enabled) {
this.debugger('Invoking service method <%s> with params: %j', this.serviceMethod.name, args);
}
const result = await toCall.apply(serviceObject, args);
if (this.postProcessors.length) {
await this.runPostProcessors(context);
}
this.processResponseHeaders(context);
await this.sendValue(result, context);
}
private getMethodToCall() {
return this.serviceClass.targetClass.prototype[this.serviceMethod.name]
|| this.serviceClass.targetClass[this.serviceMethod.name];
}
private checkAcceptance(context: ServiceContext): void {
this.debugger('Verifying accept headers');
this.identifyAcceptedLanguage(context);
this.identifyAcceptedType(context);
if (!context.accept) {
throw new Errors.NotAcceptableError('Accept');
}
if (!context.language) {
throw new Errors.NotAcceptableError('Accept-Language');
}
}
private identifyAcceptedLanguage(context: ServiceContext) {
if (this.serviceMethod.resolvedLanguages) {
const lang: any = context.request.acceptsLanguages(this.serviceMethod.resolvedLanguages);
if (lang) {
context.language = lang as string;
}
} else {
const languages: Array<string> = context.request.acceptsLanguages();
if (languages && languages.length > 0) {
context.language = languages[0];
}
}
this.debugger('Identified the preferable language accepted by server: %s', context.language);
}
private identifyAcceptedType(context: ServiceContext) {
if (this.serviceMethod.resolvedAccepts) {
context.accept = context.request.accepts(this.serviceMethod.resolvedAccepts) as string;
} else {
const accepts: Array<string> = context.request.accepts();
if (accepts && accepts.length > 0) {
context.accept = accepts[0];
}
}
this.debugger('Identified the preferable media type accepted by server: %s', context.accept);
}
private createService(context: ServiceContext) {
const serviceObject = ServerContainer.get().serviceFactory.create(this.serviceClass.targetClass, context);
this.debugger('Creating service object');
if (this.serviceClass.hasProperties()) {
this.serviceClass.properties.forEach((property, key) => {
this.debugger('Setting service property %s', key);
serviceObject[key] = this.processParameter(context, property);
});
}
return serviceObject;
}
private buildArgumentsList(context: ServiceContext) {
const result: Array<any> = new Array<any>();
this.serviceMethod.parameters.forEach(param => {
this.debugger('Processing service parameter [%s]', param.name || 'body');
result.push(this.processParameter(context, {
name: param.name,
propertyType: param.type,
type: param.paramType
}));
});
return result;
}
private processParameter(context: ServiceContext, property: ServiceProperty) {
return ParameterProcessor.get().processParameter(context, property);
}
private processResponseHeaders(context: ServiceContext) {
if (this.serviceMethod.resolvedLanguages) {
if (this.serviceMethod.httpMethod === HttpMethod.GET) {
this.debugger('Adding response header vary: Accept-Language');
context.response.vary('Accept-Language');
}
this.debugger('Adding response header Content-Language: %s', context.language);
context.response.set('Content-Language', context.language);
}
if (this.serviceMethod.resolvedAccepts) {
if (this.serviceMethod.httpMethod === HttpMethod.GET) {
this.debugger('Adding response header vary: Accept');
context.response.vary('Accept');
}
}
}
private async sendValue(value: any, context: ServiceContext) {
if (value !== NoResponse) {
this.debugger('Sending response value: %o', value);
switch (typeof value) {
case 'number':
context.response.send(value.toString());
break;
case 'string':
context.response.send(value);
break;
case 'boolean':
context.response.send(value.toString());
break;
case 'undefined':
if (!context.response.headersSent) {
context.response.sendStatus(204);
}
break;
default:
value === null
? context.response.send(value)
: await this.sendComplexValue(context, value);
}
} else {
this.debugger('Do not send any response value');
}
}
private async sendComplexValue(context: ServiceContext, value: any) {
if (value.filePath && value instanceof DownloadResource) {
await this.downloadResToPromise(context.response, value);
}
else if (value instanceof DownloadBinaryData) {
this.sendFile(context, value);
}
else if (value.location && value instanceof ReferencedResource) {
await this.sendReferencedResource(context, value);
}
else if (value.then && value.catch) {
const val = await value;
await this.sendValue(val, context);
}
else {
this.debugger('Sending a json value: %j', value);
context.response.json(value);
}
}
private async sendReferencedResource(context: ServiceContext, value: ReferencedResource<any>) {
this.debugger('Setting the header Location: %s', value.location);
this.debugger('Sendinf status code: %d', value.statusCode);
context.response.set('Location', value.location);
if (value.body) {
context.response.status(value.statusCode);
await this.sendValue(value.body, context);
}
else {
context.response.sendStatus(value.statusCode);
}
}
private sendFile(context: ServiceContext, value: DownloadBinaryData) {
this.debugger('Sending file as response');
if (value.fileName) {
context.response.writeHead(200, {
'Content-Length': value.content.length,
'Content-Type': value.mimeType,
'Content-disposition': 'attachment;filename=' + value.fileName
});
}
else {
context.response.writeHead(200, {
'Content-Length': value.content.length,
'Content-Type': value.mimeType
});
}
context.response.end(value.content);
}
private downloadResToPromise(res: express.Response, value: DownloadResource) {
this.debugger('Sending a resource to download. Path: %s', value.filePath);
return new Promise((resolve, reject) => {
res.download(value.filePath, value.fileName || value.filePath, (err) => {
if (err) {
reject(err);
} else {
resolve(undefined);
}
});
});
}
}