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 {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<RequestContext>;



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

@@ -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';


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';

declare module '../../common' {
declare module '../../../common' {
interface RequestContext {
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 {decorateRequestWithResource} from './resource';

declare module '../../common' {
declare module '../../../common' {
interface RequestContext {
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 {
resource?: Resource;
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) => {
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 {
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 {
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 {decorateRequestWithScheme} from './scheme';
import {decorateRequestWithHost} from './host';
import {decorateRequestWithBasePath} from './base-path';

declare module '../../common' {
declare module '../../../common' {
interface RequestContext {
rawUrl?: string;
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 {
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 {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;

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

@@ -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<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 === '') {
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) => {
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<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 {
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;
}

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

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

@@ -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 = (

Loading…
Cancel
Save