Browse Source

Minor refactor

Put http logic into their own directory.
master
TheoryOfNekomata 7 months ago
parent
commit
8237b750c1
13 changed files with 177 additions and 130 deletions
  1. +1
    -4
      src/backend/common.ts
  2. +1
    -1
      src/backend/core.ts
  3. +3
    -3
      src/backend/http/decorators/backend/content-negotiation.ts
  4. +2
    -2
      src/backend/http/decorators/backend/index.ts
  5. +5
    -5
      src/backend/http/decorators/backend/resource.ts
  6. +1
    -1
      src/backend/http/decorators/method/index.ts
  7. +2
    -2
      src/backend/http/decorators/url/base-path.ts
  8. +2
    -2
      src/backend/http/decorators/url/host.ts
  9. +2
    -2
      src/backend/http/decorators/url/index.ts
  10. +2
    -2
      src/backend/http/decorators/url/scheme.ts
  11. +1
    -1
      src/backend/http/handlers.ts
  12. +154
    -104
      src/backend/http/server.ts
  13. +1
    -1
      src/backend/http/utils.ts

+ 1
- 4
src/backend/common.ts View File

@@ -1,6 +1,5 @@
import {ApplicationState, ContentNegotiation, Resource} from '../common'; import {ApplicationState, ContentNegotiation, Resource} from '../common';
import {BaseDataSource} from '../common/data-source'; import {BaseDataSource} from '../common/data-source';
import http from 'http';


export interface BackendState { export interface BackendState {
app: ApplicationState; app: ApplicationState;
@@ -12,9 +11,7 @@ export interface BackendState {
showTotalItemCountOnCreateItem: boolean; showTotalItemCountOnCreateItem: boolean;
} }


export interface RequestContext extends http.IncomingMessage {
body?: unknown;
}
export interface RequestContext {}


export type RequestDecorator = (req: RequestContext) => RequestContext | Promise<RequestContext>; export type RequestDecorator = (req: RequestContext) => RequestContext | Promise<RequestContext>;




+ 1
- 1
src/backend/core.ts View File

@@ -1,7 +1,7 @@
import * as v from 'valibot'; import * as v from 'valibot';
import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common'; import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common';
import http from 'http'; import http from 'http';
import {createServer, CreateServerParams} from './server';
import {createServer, CreateServerParams} from './http/server';
import https from 'https'; import https from 'https';
import {BackendState} from './common'; import {BackendState} from './common';
import {BaseDataSource} from '../common/data-source'; import {BaseDataSource} from '../common/data-source';


src/backend/decorators/backend/content-negotiation.ts → src/backend/http/decorators/backend/content-negotiation.ts View File

@@ -1,8 +1,8 @@
import {ContentNegotiation} from '../../../common';
import {RequestDecorator} from '../../common';
import {ContentNegotiation} from '../../../../common';
import {RequestDecorator} from '../../../common';
import Negotiator from 'negotiator'; import Negotiator from 'negotiator';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
cn: ContentNegotiation; cn: ContentNegotiation;
} }

src/backend/decorators/backend/index.ts → src/backend/http/decorators/backend/index.ts View File

@@ -1,8 +1,8 @@
import {BackendState, ParamRequestDecorator} from '../../common';
import {BackendState, ParamRequestDecorator} from '../../../common';
import {decorateRequestWithContentNegotiation} from './content-negotiation'; import {decorateRequestWithContentNegotiation} from './content-negotiation';
import {decorateRequestWithResource} from './resource'; import {decorateRequestWithResource} from './resource';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
backend: BackendState; backend: BackendState;
} }

src/backend/decorators/backend/resource.ts → src/backend/http/decorators/backend/resource.ts View File

@@ -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 { interface RequestContext {
resource?: Resource; resource?: Resource;
resourceId?: string; resourceId?: string;

src/backend/decorators/method/index.ts → src/backend/http/decorators/method/index.ts View File

@@ -1,4 +1,4 @@
import {RequestDecorator} from '../../common';
import {RequestDecorator} from '../../../common';


export const decorateRequestWithMethod: RequestDecorator = (req) => { export const decorateRequestWithMethod: RequestDecorator = (req) => {
req.method = req.method?.trim().toUpperCase() ?? ''; req.method = req.method?.trim().toUpperCase() ?? '';

src/backend/decorators/url/base-path.ts → src/backend/http/decorators/url/base-path.ts View File

@@ -1,6 +1,6 @@
import {ParamRequestDecorator} from '../../common';
import {ParamRequestDecorator} from '../../../common';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
basePath: string; basePath: string;
} }

src/backend/decorators/url/host.ts → src/backend/http/decorators/url/host.ts View File

@@ -1,6 +1,6 @@
import {ParamRequestDecorator} from '../../common';
import {ParamRequestDecorator} from '../../../common';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
host: string; host: string;
} }

src/backend/decorators/url/index.ts → src/backend/http/decorators/url/index.ts View File

@@ -1,10 +1,10 @@
import {ParamRequestDecorator} from '../../common';
import {ParamRequestDecorator} from '../../../common';
import {CreateServerParams} from '../../server'; import {CreateServerParams} from '../../server';
import {decorateRequestWithScheme} from './scheme'; import {decorateRequestWithScheme} from './scheme';
import {decorateRequestWithHost} from './host'; import {decorateRequestWithHost} from './host';
import {decorateRequestWithBasePath} from './base-path'; import {decorateRequestWithBasePath} from './base-path';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
rawUrl?: string; rawUrl?: string;
query: URLSearchParams; query: URLSearchParams;

src/backend/decorators/url/scheme.ts → src/backend/http/decorators/url/scheme.ts View File

@@ -1,6 +1,6 @@
import {ParamRequestDecorator} from '../../common';
import {ParamRequestDecorator} from '../../../common';


declare module '../../common' {
declare module '../../../common' {
interface RequestContext { interface RequestContext {
scheme: string; scheme: string;
} }

src/backend/handlers.ts → src/backend/http/handlers.ts View File

@@ -2,7 +2,7 @@ import { constants } from 'http2';
import * as v from 'valibot'; import * as v from 'valibot';
import {HttpMiddlewareError, PlainResponse, Middleware} from './server'; import {HttpMiddlewareError, PlainResponse, Middleware} from './server';
import {LinkMap} from './utils'; import {LinkMap} from './utils';
import {BackendResource} from './core';
import {BackendResource} from '../core';


export const handleGetRoot: Middleware = (req) => { export const handleGetRoot: Middleware = (req) => {
const { backend, basePath } = req; const { backend, basePath } = req;

src/backend/server.ts → src/backend/http/server.ts View File

@@ -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,
}); });

src/backend/utils.ts → src/backend/http/utils.ts View File

@@ -1,5 +1,5 @@
import {IncomingMessage} from 'http'; import {IncomingMessage} from 'http';
import {MediaType, Charset} from '../common';
import {MediaType, Charset} from '../../common';
import {BaseSchema, parseAsync} from 'valibot'; import {BaseSchema, parseAsync} from 'valibot';


export const getBody = ( export const getBody = (

Loading…
Cancel
Save