From 8237b750c1ef039ba724e5a2508ece21ef12e20c Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Wed, 27 Mar 2024 19:18:43 +0800 Subject: [PATCH] Minor refactor Put http logic into their own directory. --- src/backend/common.ts | 5 +- src/backend/core.ts | 2 +- .../decorators/backend/content-negotiation.ts | 6 +- .../{ => http}/decorators/backend/index.ts | 4 +- .../{ => http}/decorators/backend/resource.ts | 10 +- .../{ => http}/decorators/method/index.ts | 2 +- .../{ => http}/decorators/url/base-path.ts | 4 +- src/backend/{ => http}/decorators/url/host.ts | 4 +- .../{ => http}/decorators/url/index.ts | 4 +- .../{ => http}/decorators/url/scheme.ts | 4 +- src/backend/{ => http}/handlers.ts | 2 +- src/backend/{ => http}/server.ts | 258 +++++++++++------- src/backend/{ => http}/utils.ts | 2 +- 13 files changed, 177 insertions(+), 130 deletions(-) rename src/backend/{ => http}/decorators/backend/content-negotiation.ts (88%) rename src/backend/{ => http}/decorators/backend/index.ts (80%) rename src/backend/{ => http}/decorators/backend/resource.ts (71%) rename src/backend/{ => http}/decorators/method/index.ts (73%) rename src/backend/{ => http}/decorators/url/base-path.ts (68%) rename src/backend/{ => http}/decorators/url/host.ts (66%) rename src/backend/{ => http}/decorators/url/index.ts (91%) rename src/backend/{ => http}/decorators/url/scheme.ts (67%) rename src/backend/{ => http}/handlers.ts (99%) rename src/backend/{ => http}/server.ts (64%) rename src/backend/{ => http}/utils.ts (95%) diff --git a/src/backend/common.ts b/src/backend/common.ts index 367db64..a54fe06 100644 --- a/src/backend/common.ts +++ b/src/backend/common.ts @@ -1,6 +1,5 @@ import {ApplicationState, ContentNegotiation, Resource} from '../common'; import {BaseDataSource} from '../common/data-source'; -import http from 'http'; export interface BackendState { app: ApplicationState; @@ -12,9 +11,7 @@ export interface BackendState { showTotalItemCountOnCreateItem: boolean; } -export interface RequestContext extends http.IncomingMessage { - body?: unknown; -} +export interface RequestContext {} export type RequestDecorator = (req: RequestContext) => RequestContext | Promise; diff --git a/src/backend/core.ts b/src/backend/core.ts index 053c25c..237cafa 100644 --- a/src/backend/core.ts +++ b/src/backend/core.ts @@ -1,7 +1,7 @@ import * as v from 'valibot'; import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common'; import http from 'http'; -import {createServer, CreateServerParams} from './server'; +import {createServer, CreateServerParams} from './http/server'; import https from 'https'; import {BackendState} from './common'; import {BaseDataSource} from '../common/data-source'; diff --git a/src/backend/decorators/backend/content-negotiation.ts b/src/backend/http/decorators/backend/content-negotiation.ts similarity index 88% rename from src/backend/decorators/backend/content-negotiation.ts rename to src/backend/http/decorators/backend/content-negotiation.ts index 7ba8f42..55081e8 100644 --- a/src/backend/decorators/backend/content-negotiation.ts +++ b/src/backend/http/decorators/backend/content-negotiation.ts @@ -1,8 +1,8 @@ -import {ContentNegotiation} from '../../../common'; -import {RequestDecorator} from '../../common'; +import {ContentNegotiation} from '../../../../common'; +import {RequestDecorator} from '../../../common'; import Negotiator from 'negotiator'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { cn: ContentNegotiation; } diff --git a/src/backend/decorators/backend/index.ts b/src/backend/http/decorators/backend/index.ts similarity index 80% rename from src/backend/decorators/backend/index.ts rename to src/backend/http/decorators/backend/index.ts index b8d52ee..c1c7132 100644 --- a/src/backend/decorators/backend/index.ts +++ b/src/backend/http/decorators/backend/index.ts @@ -1,8 +1,8 @@ -import {BackendState, ParamRequestDecorator} from '../../common'; +import {BackendState, ParamRequestDecorator} from '../../../common'; import {decorateRequestWithContentNegotiation} from './content-negotiation'; import {decorateRequestWithResource} from './resource'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { backend: BackendState; } diff --git a/src/backend/decorators/backend/resource.ts b/src/backend/http/decorators/backend/resource.ts similarity index 71% rename from src/backend/decorators/backend/resource.ts rename to src/backend/http/decorators/backend/resource.ts index 673bb3d..b91f551 100644 --- a/src/backend/decorators/backend/resource.ts +++ b/src/backend/http/decorators/backend/resource.ts @@ -1,9 +1,9 @@ -import {RequestDecorator} from '../../common'; -import {DataSource} from '../../data-source'; -import {Resource} from '../../../common'; -import {BackendResource} from '../../core'; +import {RequestDecorator} from '../../../common'; +import {DataSource} from '../../../data-source'; +import {Resource} from '../../../../common'; +import {BackendResource} from '../../../core'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { resource?: Resource; resourceId?: string; diff --git a/src/backend/decorators/method/index.ts b/src/backend/http/decorators/method/index.ts similarity index 73% rename from src/backend/decorators/method/index.ts rename to src/backend/http/decorators/method/index.ts index 412feca..520bcc4 100644 --- a/src/backend/decorators/method/index.ts +++ b/src/backend/http/decorators/method/index.ts @@ -1,4 +1,4 @@ -import {RequestDecorator} from '../../common'; +import {RequestDecorator} from '../../../common'; export const decorateRequestWithMethod: RequestDecorator = (req) => { req.method = req.method?.trim().toUpperCase() ?? ''; diff --git a/src/backend/decorators/url/base-path.ts b/src/backend/http/decorators/url/base-path.ts similarity index 68% rename from src/backend/decorators/url/base-path.ts rename to src/backend/http/decorators/url/base-path.ts index 17e230c..4182cf0 100644 --- a/src/backend/decorators/url/base-path.ts +++ b/src/backend/http/decorators/url/base-path.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../common'; +import {ParamRequestDecorator} from '../../../common'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { basePath: string; } diff --git a/src/backend/decorators/url/host.ts b/src/backend/http/decorators/url/host.ts similarity index 66% rename from src/backend/decorators/url/host.ts rename to src/backend/http/decorators/url/host.ts index 2cf8143..d6fac67 100644 --- a/src/backend/decorators/url/host.ts +++ b/src/backend/http/decorators/url/host.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../common'; +import {ParamRequestDecorator} from '../../../common'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { host: string; } diff --git a/src/backend/decorators/url/index.ts b/src/backend/http/decorators/url/index.ts similarity index 91% rename from src/backend/decorators/url/index.ts rename to src/backend/http/decorators/url/index.ts index 1bfa385..dc977f0 100644 --- a/src/backend/decorators/url/index.ts +++ b/src/backend/http/decorators/url/index.ts @@ -1,10 +1,10 @@ -import {ParamRequestDecorator} from '../../common'; +import {ParamRequestDecorator} from '../../../common'; import {CreateServerParams} from '../../server'; import {decorateRequestWithScheme} from './scheme'; import {decorateRequestWithHost} from './host'; import {decorateRequestWithBasePath} from './base-path'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { rawUrl?: string; query: URLSearchParams; diff --git a/src/backend/decorators/url/scheme.ts b/src/backend/http/decorators/url/scheme.ts similarity index 67% rename from src/backend/decorators/url/scheme.ts rename to src/backend/http/decorators/url/scheme.ts index e877e2d..784db7c 100644 --- a/src/backend/decorators/url/scheme.ts +++ b/src/backend/http/decorators/url/scheme.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../common'; +import {ParamRequestDecorator} from '../../../common'; -declare module '../../common' { +declare module '../../../common' { interface RequestContext { scheme: string; } diff --git a/src/backend/handlers.ts b/src/backend/http/handlers.ts similarity index 99% rename from src/backend/handlers.ts rename to src/backend/http/handlers.ts index 184f844..faf0cff 100644 --- a/src/backend/handlers.ts +++ b/src/backend/http/handlers.ts @@ -2,7 +2,7 @@ import { constants } from 'http2'; import * as v from 'valibot'; import {HttpMiddlewareError, PlainResponse, Middleware} from './server'; import {LinkMap} from './utils'; -import {BackendResource} from './core'; +import {BackendResource} from '../core'; export const handleGetRoot: Middleware = (req) => { const { backend, basePath } = req; diff --git a/src/backend/server.ts b/src/backend/http/server.ts similarity index 64% rename from src/backend/server.ts rename to src/backend/http/server.ts index 24d83eb..01b0763 100644 --- a/src/backend/server.ts +++ b/src/backend/http/server.ts @@ -1,8 +1,9 @@ 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 {constants} from 'http2'; +import * as v from 'valibot'; import { handleCreateItem, handleDeleteItem, @@ -14,13 +15,18 @@ import { } from './handlers'; import { BackendResource, -} from './core'; -import * as v from 'valibot'; +} from '../core'; import {getBody} from './utils'; import {decorateRequestWithBackend} from './decorators/backend'; import {decorateRequestWithMethod} from './decorators/method'; import {decorateRequestWithUrl} from './decorators/url'; +declare module '../common' { + interface RequestContext extends http.IncomingMessage { + body?: unknown; + } +} + export interface Response { statusCode: number; @@ -159,54 +165,37 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr requestTimeout: serverParams.requestTimeout, }); - const requestDecorators = [ + const defaultRequestDecorators = [ decorateRequestWithMethod, decorateRequestWithUrl(serverParams), decorateRequestWithBackend(backendState), ]; - const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse) => { - 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 === '') { - 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( + const middlewareResponse = await middlewares.reduce( async (currentHandlerStatePromise, currentValue) => { const [middlewareMethod, middleware, schema] = currentValue; try { @@ -222,8 +211,8 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr } 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 fragments = contentTypeHeader.split(';'); const mediaType = fragments[0]; @@ -257,7 +246,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr // TODO better error reporting, localizable messages // TODO handle error handlers' errors if (errRaw instanceof v.ValiError && Array.isArray(errRaw.issues)) { - return new HttpMiddlewareError('invalidResource', { + throw new HttpMiddlewareError('invalidResource', { statusCode: constants.HTTP_STATUS_BAD_REQUEST, body: errRaw.issues.map((i) => ( `${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 | HttpMiddlewareError>(middlewareState) - ) as Awaited | HttpMiddlewareError>; + Promise.resolve>(undefined) + ) as Awaited>; - if (typeof middlewareState !== 'undefined') { + if (typeof middlewareResponse === 'undefined') { + throw new HttpMiddlewareError('resourceNotFound', { + statusCode: constants.HTTP_STATUS_NOT_FOUND + }); + } + + return middlewareResponse as Awaited> + }; + + 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) => { + 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 { - 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 = { - ...( - 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 = { + ...( + 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; } - 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(); + 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; } catch (finalErrRaw) { const finalErr = finalErrRaw as HttpMiddlewareError; @@ -360,7 +410,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr ].join('; '); 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); if (typeof encoded !== 'undefined') { res.end(encoded); @@ -372,7 +422,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr } 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, { Allow: middlewares.map((m) => m[0]).join(', '), '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 - 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, { 'Content-Language': req.backend.cn.language.name, }); diff --git a/src/backend/utils.ts b/src/backend/http/utils.ts similarity index 95% rename from src/backend/utils.ts rename to src/backend/http/utils.ts index e9e733b..09c3a49 100644 --- a/src/backend/utils.ts +++ b/src/backend/http/utils.ts @@ -1,5 +1,5 @@ import {IncomingMessage} from 'http'; -import {MediaType, Charset} from '../common'; +import {MediaType, Charset} from '../../common'; import {BaseSchema, parseAsync} from 'valibot'; export const getBody = (