|
@@ -1,8 +1,9 @@ |
|
|
import http from 'http'; |
|
|
import http from 'http'; |
|
|
import {BackendState, RequestContext} from './common'; |
|
|
|
|
|
import {Language, Resource, LanguageStatusMessageMap} from '../common'; |
|
|
|
|
|
|
|
|
import {BackendState, RequestContext} from '../common'; |
|
|
|
|
|
import {Language, Resource, LanguageStatusMessageMap} from '../../common'; |
|
|
import https from 'https'; |
|
|
import https from 'https'; |
|
|
import {constants} from 'http2'; |
|
|
import {constants} from 'http2'; |
|
|
|
|
|
import * as v from 'valibot'; |
|
|
import { |
|
|
import { |
|
|
handleCreateItem, |
|
|
handleCreateItem, |
|
|
handleDeleteItem, |
|
|
handleDeleteItem, |
|
@@ -14,13 +15,18 @@ import { |
|
|
} from './handlers'; |
|
|
} from './handlers'; |
|
|
import { |
|
|
import { |
|
|
BackendResource, |
|
|
BackendResource, |
|
|
} from './core'; |
|
|
|
|
|
import * as v from 'valibot'; |
|
|
|
|
|
|
|
|
} from '../core'; |
|
|
import {getBody} from './utils'; |
|
|
import {getBody} from './utils'; |
|
|
import {decorateRequestWithBackend} from './decorators/backend'; |
|
|
import {decorateRequestWithBackend} from './decorators/backend'; |
|
|
import {decorateRequestWithMethod} from './decorators/method'; |
|
|
import {decorateRequestWithMethod} from './decorators/method'; |
|
|
import {decorateRequestWithUrl} from './decorators/url'; |
|
|
import {decorateRequestWithUrl} from './decorators/url'; |
|
|
|
|
|
|
|
|
|
|
|
declare module '../common' { |
|
|
|
|
|
interface RequestContext extends http.IncomingMessage { |
|
|
|
|
|
body?: unknown; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export interface Response { |
|
|
export interface Response { |
|
|
statusCode: number; |
|
|
statusCode: number; |
|
|
|
|
|
|
|
@@ -159,54 +165,37 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
requestTimeout: serverParams.requestTimeout, |
|
|
requestTimeout: serverParams.requestTimeout, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const requestDecorators = [ |
|
|
|
|
|
|
|
|
const defaultRequestDecorators = [ |
|
|
decorateRequestWithMethod, |
|
|
decorateRequestWithMethod, |
|
|
decorateRequestWithUrl(serverParams), |
|
|
decorateRequestWithUrl(serverParams), |
|
|
decorateRequestWithBackend(backendState), |
|
|
decorateRequestWithBackend(backendState), |
|
|
]; |
|
|
]; |
|
|
|
|
|
|
|
|
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { |
|
|
|
|
|
let req: RequestContext; |
|
|
|
|
|
// TODO custom decorators |
|
|
|
|
|
const effectiveRequestDecorators = requestDecorators; |
|
|
|
|
|
req = await effectiveRequestDecorators.reduce( |
|
|
|
|
|
async (resultRequestPromise, decorator) => { |
|
|
|
|
|
const resultRequest = await resultRequestPromise; |
|
|
|
|
|
|
|
|
|
|
|
return await decorator(resultRequest); |
|
|
|
|
|
}, |
|
|
|
|
|
Promise.resolve(reqRaw) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
let middlewareState; |
|
|
|
|
|
|
|
|
const processRequest = (middlewares: [string, Middleware, v.BaseSchema?][]) => async (req: RequestContext) => { |
|
|
if (req.url === '/' || req.url === '') { |
|
|
if (req.url === '/' || req.url === '') { |
|
|
middlewareState = await handleGetRoot(req); |
|
|
|
|
|
|
|
|
return handleGetRoot(req); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let resource = req.resource as BackendResource | undefined; |
|
|
|
|
|
if (typeof middlewareState === 'undefined') { |
|
|
|
|
|
if (typeof resource === 'undefined') { |
|
|
|
|
|
res.statusCode = constants.HTTP_STATUS_NOT_FOUND; |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound; |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (typeof req.resource === 'undefined') { |
|
|
|
|
|
throw new HttpMiddlewareError('resourceNotFound', { |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_NOT_FOUND |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
await resource.dataSource.initialize(); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError( |
|
|
|
|
|
'unableToInitializeResourceDataSource', |
|
|
|
|
|
{ |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const resource = req.resource as BackendResource; |
|
|
|
|
|
try { |
|
|
|
|
|
await resource.dataSource.initialize(); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError( |
|
|
|
|
|
'unableToInitializeResourceDataSource', |
|
|
|
|
|
{ |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId ?? ''); |
|
|
|
|
|
middlewareState = await middlewares.reduce<unknown>( |
|
|
|
|
|
|
|
|
const middlewareResponse = await middlewares.reduce<unknown>( |
|
|
async (currentHandlerStatePromise, currentValue) => { |
|
|
async (currentHandlerStatePromise, currentValue) => { |
|
|
const [middlewareMethod, middleware, schema] = currentValue; |
|
|
const [middlewareMethod, middleware, schema] = currentValue; |
|
|
try { |
|
|
try { |
|
@@ -222,8 +211,8 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (schema) { |
|
|
if (schema) { |
|
|
const availableSerializers = Array.from(req.backend!.app.mediaTypes.values()); |
|
|
|
|
|
const availableCharsets = Array.from(req.backend!.app.charsets.values()); |
|
|
|
|
|
|
|
|
const availableSerializers = Array.from(req.backend.app.mediaTypes.values()); |
|
|
|
|
|
const availableCharsets = Array.from(req.backend.app.charsets.values()); |
|
|
const contentTypeHeader = req.headers['content-type'] ?? 'application/octet-stream'; |
|
|
const contentTypeHeader = req.headers['content-type'] ?? 'application/octet-stream'; |
|
|
const fragments = contentTypeHeader.split(';'); |
|
|
const fragments = contentTypeHeader.split(';'); |
|
|
const mediaType = fragments[0]; |
|
|
const mediaType = fragments[0]; |
|
@@ -257,7 +246,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
// TODO better error reporting, localizable messages |
|
|
// TODO better error reporting, localizable messages |
|
|
// TODO handle error handlers' errors |
|
|
// TODO handle error handlers' errors |
|
|
if (errRaw instanceof v.ValiError && Array.isArray(errRaw.issues)) { |
|
|
if (errRaw instanceof v.ValiError && Array.isArray(errRaw.issues)) { |
|
|
return new HttpMiddlewareError('invalidResource', { |
|
|
|
|
|
|
|
|
throw new HttpMiddlewareError('invalidResource', { |
|
|
statusCode: constants.HTTP_STATUS_BAD_REQUEST, |
|
|
statusCode: constants.HTTP_STATUS_BAD_REQUEST, |
|
|
body: errRaw.issues.map((i) => ( |
|
|
body: errRaw.issues.map((i) => ( |
|
|
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` |
|
|
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` |
|
@@ -265,74 +254,135 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return errRaw; |
|
|
|
|
|
|
|
|
throw errRaw; |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
Promise.resolve<ReturnType<Middleware> | HttpMiddlewareError>(middlewareState) |
|
|
|
|
|
) as Awaited<ReturnType<Middleware> | HttpMiddlewareError>; |
|
|
|
|
|
|
|
|
Promise.resolve<ReturnType<Middleware>>(undefined) |
|
|
|
|
|
) as Awaited<ReturnType<Middleware>>; |
|
|
|
|
|
|
|
|
if (typeof middlewareState !== 'undefined') { |
|
|
|
|
|
|
|
|
if (typeof middlewareResponse === 'undefined') { |
|
|
|
|
|
throw new HttpMiddlewareError('resourceNotFound', { |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_NOT_FOUND |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return middlewareResponse as Awaited<ReturnType<Middleware>> |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const decorateRequest = async (reqRaw: http.IncomingMessage) => { |
|
|
|
|
|
// TODO custom decorators |
|
|
|
|
|
const effectiveRequestDecorators = defaultRequestDecorators; |
|
|
|
|
|
return await effectiveRequestDecorators.reduce( |
|
|
|
|
|
async (resultRequestPromise, decorator) => { |
|
|
|
|
|
const resultRequest = await resultRequestPromise; |
|
|
|
|
|
|
|
|
|
|
|
return await decorator(resultRequest); |
|
|
|
|
|
}, |
|
|
|
|
|
Promise.resolve(reqRaw as RequestContext) |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { |
|
|
|
|
|
const req = await decorateRequest(reqRaw); |
|
|
|
|
|
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId ?? ''); |
|
|
|
|
|
const processRequestFn = processRequest(middlewares); |
|
|
|
|
|
let middlewareState: Response; |
|
|
|
|
|
try { |
|
|
|
|
|
middlewareState = await processRequestFn(req) as any; // TODO fix this |
|
|
|
|
|
} catch (processRequestErrRaw) { |
|
|
|
|
|
const finalErr = processRequestErrRaw as HttpMiddlewareError; |
|
|
|
|
|
const headers = finalErr.response.headers ?? {}; |
|
|
|
|
|
let encoded: Buffer | undefined; |
|
|
|
|
|
let serialized; |
|
|
try { |
|
|
try { |
|
|
if (middlewareState instanceof Error) { |
|
|
|
|
|
throw middlewareState; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
serialized = typeof finalErr.response.body !== 'undefined' ? req.backend.cn.mediaType.serialize(finalErr.response.body) : undefined; |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const headers: Record<string, string> = { |
|
|
|
|
|
...( |
|
|
|
|
|
middlewareState.headers ?? {} |
|
|
|
|
|
), |
|
|
|
|
|
'Content-Language': req.cn.language.name |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
encoded = typeof serialized !== 'undefined' ? req.backend.cn.charset.encode(serialized) : undefined; |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); |
|
|
|
|
|
res.end(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
headers['Content-Type'] = [ |
|
|
|
|
|
req.backend.cn.mediaType.name, |
|
|
|
|
|
`charset=${req.backend.cn.charset.name}` |
|
|
|
|
|
].join('; '); |
|
|
|
|
|
|
|
|
if (middlewareState instanceof http.ServerResponse) { |
|
|
|
|
|
// TODO streaming responses |
|
|
|
|
|
middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); |
|
|
|
|
|
|
|
|
const statusMessageKey = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined; |
|
|
|
|
|
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
|
|
|
res.writeHead(finalErr.response.statusCode, headers); |
|
|
|
|
|
if (typeof encoded !== 'undefined') { |
|
|
|
|
|
res.end(encoded); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const headers: Record<string, string> = { |
|
|
|
|
|
...( |
|
|
|
|
|
middlewareState.headers ?? {} |
|
|
|
|
|
), |
|
|
|
|
|
'Content-Language': req.cn.language.name |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
if (middlewareState instanceof http.ServerResponse) { |
|
|
|
|
|
// TODO streaming responses |
|
|
|
|
|
middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (middlewareState instanceof PlainResponse) { |
|
|
|
|
|
let encoded: Buffer | undefined; |
|
|
|
|
|
if (typeof middlewareState.body !== 'undefined') { |
|
|
|
|
|
let serialized; |
|
|
|
|
|
try { |
|
|
|
|
|
serialized = req.cn.mediaType.serialize(middlewareState.body); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}); |
|
|
|
|
|
res.end(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (middlewareState instanceof PlainResponse) { |
|
|
|
|
|
let encoded: Buffer | undefined; |
|
|
|
|
|
if (typeof middlewareState.body !== 'undefined') { |
|
|
|
|
|
let serialized; |
|
|
|
|
|
try { |
|
|
|
|
|
serialized = req.cn.mediaType.serialize(middlewareState.body); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError('unableToSerializeResponse', { |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
headers: { |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
encoded = req.cn.charset.encode(serialized); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError('unableToEncodeResponse', { |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
headers: { |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
headers['Content-Type'] = [ |
|
|
|
|
|
req.cn.mediaType.name, |
|
|
|
|
|
`charset=${req.cn.charset.name}` |
|
|
|
|
|
].join('; '); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const statusMessageKey = middlewareState.statusMessage ? req.cn.language.statusMessages[middlewareState.statusMessage] : undefined; |
|
|
|
|
|
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resource!.state.itemName) ?? ''; |
|
|
|
|
|
res.writeHead(middlewareState.statusCode, headers); |
|
|
|
|
|
if (typeof encoded !== 'undefined') { |
|
|
|
|
|
res.end(encoded); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
encoded = req.cn.charset.encode(serialized); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}); |
|
|
res.end(); |
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
headers['Content-Type'] = [ |
|
|
|
|
|
req.cn.mediaType.name, |
|
|
|
|
|
`charset=${req.cn.charset.name}` |
|
|
|
|
|
].join('; '); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const statusMessageKey = middlewareState.statusMessage ? req.cn.language.statusMessages[middlewareState.statusMessage] : undefined; |
|
|
|
|
|
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
|
|
|
res.writeHead(middlewareState.statusCode, headers); |
|
|
|
|
|
if (typeof encoded !== 'undefined') { |
|
|
|
|
|
res.end(encoded); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (typeof middlewareState !== 'undefined') { |
|
|
|
|
|
try { |
|
|
return; |
|
|
return; |
|
|
} catch (finalErrRaw) { |
|
|
} catch (finalErrRaw) { |
|
|
const finalErr = finalErrRaw as HttpMiddlewareError; |
|
|
const finalErr = finalErrRaw as HttpMiddlewareError; |
|
@@ -360,7 +410,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
].join('; '); |
|
|
].join('; '); |
|
|
|
|
|
|
|
|
const statusMessageKey = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined; |
|
|
const statusMessageKey = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined; |
|
|
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resource!.state.itemName) ?? ''; |
|
|
|
|
|
|
|
|
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
res.writeHead(finalErr.response.statusCode, headers); |
|
|
res.writeHead(finalErr.response.statusCode, headers); |
|
|
if (typeof encoded !== 'undefined') { |
|
|
if (typeof encoded !== 'undefined') { |
|
|
res.end(encoded); |
|
|
res.end(encoded); |
|
@@ -372,7 +422,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (middlewares.length > 0) { |
|
|
if (middlewares.length > 0) { |
|
|
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g, resource!.state.itemName) ?? ''; |
|
|
|
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { |
|
|
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { |
|
|
Allow: middlewares.map((m) => m[0]).join(', '), |
|
|
Allow: middlewares.map((m) => m[0]).join(', '), |
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
'Content-Language': req.backend.cn.language.name, |
|
@@ -382,7 +432,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// TODO error handler in line with authentication |
|
|
// TODO error handler in line with authentication |
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g, resource!.state.itemName) ?? ''; |
|
|
|
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? ''; |
|
|
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { |
|
|
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { |
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
}); |
|
|
}); |