@@ -6,8 +6,8 @@ import {
AllowedMiddlewareSpecification,
AllowedMiddlewareSpecification,
BackendState,
BackendState,
Middleware,
Middleware,
RequestContext,
Response
RequestContext, RequestDecorator,
Response,
} from '../../common';
} from '../../common';
import {Resource} from '../../../common';
import {Resource} from '../../../common';
import {
import {
@@ -142,7 +142,24 @@ class CqrsEventEmitter extends EventEmitter {
}
}
interface ServerState {
requestDecorators: Set<RequestDecorator>;
}
export interface Server {
readonly listening: boolean;
on(event: string, cb: (...args: unknown[]) => unknown): this;
close(callback?: (err?: Error) => void): this;
listen(...args: Parameters<http.Server['listen']>): this;
requestDecorator(requestDecorator: RequestDecorator): this;
defaultErrorHandler(): this;
}
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
const state: ServerState = {
requestDecorators: new Set<RequestDecorator>(),
};
const isHttps = 'key' in serverParams && 'cert' in serverParams;
const isHttps = 'key' in serverParams && 'cert' in serverParams;
const theRes = new CqrsEventEmitter();
const theRes = new CqrsEventEmitter();
@@ -156,12 +173,6 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
requestTimeout: serverParams.requestTimeout,
requestTimeout: serverParams.requestTimeout,
});
});
const defaultRequestDecorators = [
decorateRequestWithMethod,
decorateRequestWithUrl(serverParams),
decorateRequestWithBackend(backendState),
];
const handleMiddlewares = async (currentHandlerState: Awaited<ReturnType<Middleware>>, currentMiddleware: AllowedMiddlewareSpecification, req: ResourceRequestContext) => {
const handleMiddlewares = async (currentHandlerState: Awaited<ReturnType<Middleware>>, currentMiddleware: AllowedMiddlewareSpecification, req: ResourceRequestContext) => {
const { method: middlewareMethod, middleware, constructBodySchema} = currentMiddleware;
const { method: middlewareMethod, middleware, constructBodySchema} = currentMiddleware;
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method;
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method;
@@ -305,9 +316,18 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
return middlewareResponse as Awaited<ReturnType<Middleware>>
return middlewareResponse as Awaited<ReturnType<Middleware>>
};
};
const defaultRequestDecorators = [
decorateRequestWithMethod,
decorateRequestWithUrl(serverParams),
decorateRequestWithBackend(backendState),
];
const decorateRequest = async (reqRaw: http.IncomingMessage) => {
const decorateRequest = async (reqRaw: http.IncomingMessage) => {
// TODO custom decorators
const effectiveRequestDecorators = defaultRequestDecorators;
const effectiveRequestDecorators = [
...defaultRequestDecorators,
...Array.from(state.requestDecorators),
];
return await effectiveRequestDecorators.reduce(
return await effectiveRequestDecorators.reduce(
async (resultRequestPromise, decorator) => {
async (resultRequestPromise, decorator) => {
const resultRequest = await resultRequestPromise;
const resultRequest = await resultRequestPromise;
@@ -319,10 +339,51 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
);
);
};
};
const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {};
let encoded: Buffer | undefined;
let serialized;
try {
serialized = typeof finalErr.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.body) : undefined;
} catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}
try {
encoded = typeof serialized !== 'undefined' ? resourceReq.backend.cn.charset.encode(serialized) : undefined;
} catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
}
headers['Content-Type'] = [
resourceReq.backend.cn.mediaType.name,
`charset=${resourceReq.backend.cn.charset.name}`,
].join('; ');
const statusMessageKey = finalErr.statusMessage ? resourceReq.backend.cn.language.statusMessages[finalErr.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? '';
res.writeHead(finalErr.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
}
res.end();
};
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
if (typeof plainReq.resource !== 'undefined') {
if (typeof plainReq.resource !== 'undefined') {
const resourceReq = plainReq as ResourceRequestContext;
const resourceReq = plainReq as ResourceRequestContext;
// TODO custom middlewares
const effectiveMiddlewares = (
const effectiveMiddlewares = (
typeof resourceReq.resourceId === 'string'
typeof resourceReq.resourceId === 'string'
? defaultItemMiddlewares
? defaultItemMiddlewares
@@ -335,43 +396,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
try {
try {
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
} catch (processRequestErrRaw) {
} catch (processRequestErrRaw) {
const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {};
let encoded: Buffer | undefined;
let serialized;
try {
serialized = typeof finalErr.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.body) : undefined;
} catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}
try {
encoded = typeof serialized !== 'undefined' ? resourceReq.backend.cn.charset.encode(serialized) : undefined;
} catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
}
headers['Content-Type'] = [
resourceReq.backend.cn.mediaType.name,
`charset=${resourceReq.backend.cn.charset.name}`,
].join('; ');
const statusMessageKey = finalErr.statusMessage ? resourceReq.backend.cn.language.statusMessages[finalErr.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? '';
res.writeHead(finalErr.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
}
res.end();
handleMiddlewareError(processRequestErrRaw as Error)(resourceReq, res);
return;
return;
}
}
@@ -476,5 +501,28 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
server.on('request', handleRequest);
server.on('request', handleRequest);
return server;
return {
get listening() { return server.listening },
listen(...args: Parameters<Server['listen']>) {
server.listen(...args);
return this;
},
close(callback?: (err?: Error) => void) {
server.close(callback);
return this;
},
on(...args: Parameters<Server['on']>) {
server.on(args[0], args[1]);
return this;
},
requestDecorator(requestDecorator: RequestDecorator) {
state.requestDecorators.add(requestDecorator);
return this;
},
defaultErrorHandler() {
return this;
}
} satisfies Server;
// return server;
}
}