Browse Source

Add alternate way of responses

Add CQRS event emitter for long-running operations.
master
TheoryOfNekomata 6 months ago
parent
commit
8bb5aa9cca
7 changed files with 133 additions and 73 deletions
  1. +1
    -1
      src/backend/common.ts
  2. +33
    -16
      src/backend/servers/http/core.ts
  3. +1
    -1
      src/backend/servers/http/decorators/url/index.ts
  4. +9
    -6
      src/backend/servers/http/handlers/default.ts
  5. +73
    -44
      src/backend/servers/http/handlers/resource.ts
  6. +14
    -5
      src/backend/servers/http/response.ts
  7. +2
    -0
      src/common/language.ts

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

@@ -35,11 +35,11 @@ export abstract class MiddlewareResponseError extends MiddlewareError implements
} }
} }



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


export type ParamRequestDecorator<Params extends Array<unknown> = []> = (...args: Params) => RequestDecorator; export type ParamRequestDecorator<Params extends Array<unknown> = []> = (...args: Params) => RequestDecorator;


// TODO put this in HTTP
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';


export interface AllowedMiddlewareSpecification<Schema extends BaseSchema = BaseSchema> { export interface AllowedMiddlewareSpecification<Schema extends BaseSchema = BaseSchema> {


+ 33
- 16
src/backend/servers/http/core.ts View File

@@ -25,7 +25,8 @@ import {getBody, isTextMediaType} 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';
import {HttpMiddlewareError, PlainResponse} from './response';
import {ErrorPlainResponse, PlainResponse} from './response';
import EventEmitter from 'events';


type RequiredResource = Required<Pick<RequestContext, 'resource'>>['resource']; type RequiredResource = Required<Pick<RequestContext, 'resource'>>['resource'];


@@ -42,8 +43,8 @@ declare module '../../common' {
body?: unknown; body?: unknown;
} }


interface Middleware<Req extends ResourceRequestContext = ResourceRequestContext> {
(req: Req): undefined | Response | Promise<undefined | Response>;
interface Middleware<Req extends ResourceRequestContext = ResourceRequestContext, Res extends NodeJS.EventEmitter = NodeJS.EventEmitter> {
(req: Req, res: Res): undefined | Response | Promise<undefined | Response>;
} }
} }


@@ -137,8 +138,13 @@ export interface CreateServerParams {
streamResponses?: boolean; streamResponses?: boolean;
} }


class CqrsEventEmitter extends EventEmitter {

}

export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
const isHttps = 'key' in serverParams && 'cert' in serverParams; const isHttps = 'key' in serverParams && 'cert' in serverParams;
const theRes = new CqrsEventEmitter();


const server = isHttps const server = isHttps
? https.createServer({ ? https.createServer({
@@ -196,14 +202,16 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
const theBodyBuffer = await getBody(req); const theBodyBuffer = await getBody(req);
const encodingPair = req.backend.app.charsets.get(charset); const encodingPair = req.backend.app.charsets.get(charset);
if (typeof encodingPair === 'undefined') { if (typeof encodingPair === 'undefined') {
throw new HttpMiddlewareError('unableToDecodeResource', {
throw new ErrorPlainResponse('unableToDecodeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
res: theRes,
}); });
} }
const deserializerPair = req.backend.app.mediaTypes.get(mediaType); const deserializerPair = req.backend.app.mediaTypes.get(mediaType);
if (typeof deserializerPair === 'undefined') { if (typeof deserializerPair === 'undefined') {
throw new HttpMiddlewareError('unableToDeserializeResource', {
throw new ErrorPlainResponse('unableToDeserializeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
res: theRes,
}); });
} }
const theBodyStr = encodingPair.decode(theBodyBuffer); const theBodyStr = encodingPair.decode(theBodyBuffer);
@@ -216,23 +224,27 @@ 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 (Array.isArray(err.issues)) { if (Array.isArray(err.issues)) {
throw new HttpMiddlewareError('invalidResource', {
throw new ErrorPlainResponse('invalidResource', {
statusCode: constants.HTTP_STATUS_BAD_REQUEST, statusCode: constants.HTTP_STATUS_BAD_REQUEST,
body: err.issues.map((i) => ( body: err.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}`
)), )),
res: theRes,
}); });
} }
} }
} }


const result = await middleware(req);
const result = await middleware(req, theRes);


// HEAD is just GET without the response body // HEAD is just GET without the response body
if (req.method === 'HEAD' && result instanceof PlainResponse) { if (req.method === 'HEAD' && result instanceof PlainResponse) {
const { body: _, ...etcResult } = result; const { body: _, ...etcResult } = result;


return new PlainResponse(etcResult);
return new PlainResponse({
...etcResult,
res: theRes,
});
} }


return result; return result;
@@ -240,34 +252,37 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr


const processRequest = (middlewares: AllowedMiddlewareSpecification[]) => async (req: ResourceRequestContext) => { const processRequest = (middlewares: AllowedMiddlewareSpecification[]) => async (req: ResourceRequestContext) => {
if (req.url === '/' || req.url === '') { if (req.url === '/' || req.url === '') {
return handleGetRoot(req);
return handleGetRoot(req, theRes);
} }


const { resource } = req; const { resource } = req;
if (typeof resource === 'undefined') { if (typeof resource === 'undefined') {
throw new HttpMiddlewareError('resourceNotFound', {
throw new ErrorPlainResponse('resourceNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND, statusCode: constants.HTTP_STATUS_NOT_FOUND,
res: theRes,
}); });
} }


if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return handleOptions(middlewares)(req);
return handleOptions(middlewares)(req, theRes);
} }


if (typeof resource.dataSource === 'undefined') { if (typeof resource.dataSource === 'undefined') {
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', {
throw new ErrorPlainResponse('unableToInitializeResourceDataSource', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res: theRes,
}); });
} }


try { try {
await resource.dataSource.initialize(); await resource.dataSource.initialize();
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'unableToInitializeResourceDataSource', 'unableToInitializeResourceDataSource',
{ {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res: theRes,
} }
); );
} }
@@ -281,8 +296,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
) as Awaited<ReturnType<Middleware>>; ) as Awaited<ReturnType<Middleware>>;


if (typeof middlewareResponse === 'undefined') { if (typeof middlewareResponse === 'undefined') {
throw new HttpMiddlewareError('resourceNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND
throw new ErrorPlainResponse('resourceNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res: theRes,
}); });
} }


@@ -313,12 +329,13 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
: defaultCollectionMiddlewares : defaultCollectionMiddlewares
); );
const middlewares = effectiveMiddlewares.filter((m) => m.allowed(resourceReq.resource)); const middlewares = effectiveMiddlewares.filter((m) => m.allowed(resourceReq.resource));
// TODO listen to res.on('response')
const processRequestFn = processRequest(middlewares); const processRequestFn = processRequest(middlewares);
let middlewareState: Response; let middlewareState: Response;
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 HttpMiddlewareError;
const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {}; const headers = finalErr.headers ?? {};
let encoded: Buffer | undefined; let encoded: Buffer | undefined;
let serialized; let serialized;


+ 1
- 1
src/backend/servers/http/decorators/url/index.ts View File

@@ -1,5 +1,5 @@
import {ParamRequestDecorator} from '../../../../common'; import {ParamRequestDecorator} from '../../../../common';
import {CreateServerParams} from '../../index';
import {CreateServerParams} from '../../core';
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';


+ 9
- 6
src/backend/servers/http/handlers/default.ts View File

@@ -1,9 +1,9 @@
import {constants} from 'http2'; import {constants} from 'http2';
import {AllowedMiddlewareSpecification, Middleware} from '../../../common'; import {AllowedMiddlewareSpecification, Middleware} from '../../../common';
import {LinkMap} from '../utils'; import {LinkMap} from '../utils';
import {PlainResponse, HttpMiddlewareError} from '../response';
import {PlainResponse, ErrorPlainResponse} from '../response';


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


const data = { const data = {
@@ -35,23 +35,26 @@ export const handleGetRoot: Middleware = (req) => {
headers, headers,
statusMessage: 'ok', statusMessage: 'ok',
statusCode: constants.HTTP_STATUS_OK, statusCode: constants.HTTP_STATUS_OK,
body: data
body: data,
res,
}); });
}; };


export const handleOptions = (middlewares: AllowedMiddlewareSpecification[]): Middleware => () => {
export const handleOptions = (middlewares: AllowedMiddlewareSpecification[]): Middleware => (_req, res) => {
if (middlewares.length > 0) { if (middlewares.length > 0) {
return new PlainResponse({ return new PlainResponse({
headers: { headers: {
'Allow': middlewares.flatMap((m) => m.method === 'GET' ? [m.method, 'HEAD'] : [m.method]).join(', '), 'Allow': middlewares.flatMap((m) => m.method === 'GET' ? [m.method, 'HEAD'] : [m.method]).join(', '),
}, },
statusMessage: 'ok',
statusMessage: 'provideOptions',
statusCode: constants.HTTP_STATUS_NO_CONTENT, statusCode: constants.HTTP_STATUS_NO_CONTENT,
res,
}); });
} }


// TODO add option for custom error handler // TODO add option for custom error handler
throw new HttpMiddlewareError('methodNotAllowed', {
throw new ErrorPlainResponse('methodNotAllowed', {
statusCode: constants.HTTP_STATUS_METHOD_NOT_ALLOWED, statusCode: constants.HTTP_STATUS_METHOD_NOT_ALLOWED,
res,
}); });
}; };

+ 73
- 44
src/backend/servers/http/handlers/resource.ts View File

@@ -1,9 +1,9 @@
import { constants } from 'http2'; import { constants } from 'http2';
import * as v from 'valibot'; import * as v from 'valibot';
import {Middleware} from '../../../common'; import {Middleware} from '../../../common';
import {HttpMiddlewareError, PlainResponse} from '../response';
import {ErrorPlainResponse, PlainResponse} from '../response';


export const handleGetCollection: Middleware = async (req) => {
export const handleGetCollection: Middleware = async (req, res) => {
const { query, resource, backend } = req; const { query, resource, backend } = req;


let data: v.Output<typeof resource.schema>[]; let data: v.Output<typeof resource.schema>[];
@@ -15,11 +15,12 @@ export const handleGetCollection: Middleware = async (req) => {
totalItemCount = await resource.dataSource.getTotalCount(query); totalItemCount = await resource.dataSource.getTotalCount(query);
} }
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'unableToFetchResourceCollection', 'unableToFetchResourceCollection',
{ {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
} }
); );
} }
@@ -34,17 +35,19 @@ export const handleGetCollection: Middleware = async (req) => {
statusCode: constants.HTTP_STATUS_OK, statusCode: constants.HTTP_STATUS_OK,
statusMessage: 'resourceCollectionFetched', statusMessage: 'resourceCollectionFetched',
body: data, body: data,
res,
}); });
}; };


export const handleGetItem: Middleware = async (req) => {
export const handleGetItem: Middleware = async (req, res) => {
const { resource, resourceId } = req; const { resource, resourceId } = req;


if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) { if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'resourceIdNotGiven', 'resourceIdNotGiven',
{ {
statusCode: constants.HTTP_STATUS_BAD_REQUEST, statusCode: constants.HTTP_STATUS_BAD_REQUEST,
res,
} }
); );
} }
@@ -53,39 +56,43 @@ export const handleGetItem: Middleware = async (req) => {
try { try {
data = await resource.dataSource.getById(resourceId); data = await resource.dataSource.getById(resourceId);
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'unableToFetchResource', 'unableToFetchResource',
{ {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
} }
); );
} }


if (typeof data !== 'undefined' && data !== null) {
return new PlainResponse({
statusCode: constants.HTTP_STATUS_OK,
statusMessage: 'resourceFetched',
body: data
});
if (!(typeof data !== 'undefined' && data !== null)) {
throw new ErrorPlainResponse(
'resourceNotFound',
{
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
},
);
} }


throw new HttpMiddlewareError(
'resourceNotFound',
{
statusCode: constants.HTTP_STATUS_NOT_FOUND,
}
);
return new PlainResponse({
statusCode: constants.HTTP_STATUS_OK,
statusMessage: 'resourceFetched',
body: data,
res,
});
}; };


export const handleDeleteItem: Middleware = async (req) => {
export const handleDeleteItem: Middleware = async (req, res) => {
const { resource, resourceId, backend } = req; const { resource, resourceId, backend } = req;


if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) { if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'resourceIdNotGiven', 'resourceIdNotGiven',
{ {
statusCode: constants.HTTP_STATUS_BAD_REQUEST, statusCode: constants.HTTP_STATUS_BAD_REQUEST,
res,
} }
); );
} }
@@ -94,15 +101,17 @@ export const handleDeleteItem: Middleware = async (req) => {
try { try {
existing = await resource.dataSource.getById(resourceId); existing = await resource.dataSource.getById(resourceId);
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToFetchResource', {
throw new ErrorPlainResponse('unableToFetchResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


if (!existing && backend!.throwsErrorOnDeletingNotFound) { if (!existing && backend!.throwsErrorOnDeletingNotFound) {
throw new HttpMiddlewareError('deleteNonExistingResource', {
statusCode: constants.HTTP_STATUS_NOT_FOUND
throw new ErrorPlainResponse('deleteNonExistingResource', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res
}); });
} }


@@ -111,26 +120,29 @@ export const handleDeleteItem: Middleware = async (req) => {
await resource.dataSource.delete(resourceId); await resource.dataSource.delete(resourceId);
} }
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToDeleteResource', {
throw new ErrorPlainResponse('unableToDeleteResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res
}) })
} }


return new PlainResponse({ return new PlainResponse({
statusCode: constants.HTTP_STATUS_NO_CONTENT, statusCode: constants.HTTP_STATUS_NO_CONTENT,
statusMessage: 'resourceDeleted', statusMessage: 'resourceDeleted',
res,
}); });
}; };


export const handlePatchItem: Middleware = async (req) => {
export const handlePatchItem: Middleware = async (req, res) => {
const { resource, resourceId, body } = req; const { resource, resourceId, body } = req;


if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) { if (typeof resourceId === 'undefined' || resourceId.trim().length < 1) {
throw new HttpMiddlewareError(
throw new ErrorPlainResponse(
'resourceIdNotGiven', 'resourceIdNotGiven',
{ {
statusCode: constants.HTTP_STATUS_BAD_REQUEST, statusCode: constants.HTTP_STATUS_BAD_REQUEST,
res,
} }
); );
} }
@@ -139,15 +151,17 @@ export const handlePatchItem: Middleware = async (req) => {
try { try {
existing = await resource.dataSource.getById(resourceId!); existing = await resource.dataSource.getById(resourceId!);
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToFetchResource', {
throw new ErrorPlainResponse('unableToFetchResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


if (!existing) { if (!existing) {
throw new HttpMiddlewareError('patchNonExistingResource', {
throw new ErrorPlainResponse('patchNonExistingResource', {
statusCode: constants.HTTP_STATUS_NOT_FOUND, statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}); });
} }


@@ -155,9 +169,10 @@ export const handlePatchItem: Middleware = async (req) => {
try { try {
newObject = await resource.dataSource.patch(resourceId!, body as object); newObject = await resource.dataSource.patch(resourceId!, body as object);
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToPatchResource', {
throw new ErrorPlainResponse('unableToPatchResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


@@ -165,16 +180,18 @@ export const handlePatchItem: Middleware = async (req) => {
statusCode: constants.HTTP_STATUS_OK, statusCode: constants.HTTP_STATUS_OK,
statusMessage: 'resourcePatched', statusMessage: 'resourcePatched',
body: newObject, body: newObject,
res,
}); });
}; };


export const handleCreateItem: Middleware = async (req) => {
export const handleCreateItem: Middleware = async (req, res) => {
const { resource, body, backend, basePath } = req; const { resource, body, backend, basePath } = req;


const idAttrRaw = resource.state.shared.get('idAttr'); const idAttrRaw = resource.state.shared.get('idAttr');
if (typeof idAttrRaw === 'undefined') { if (typeof idAttrRaw === 'undefined') {
throw new HttpMiddlewareError('unableToGenerateIdFromResourceDataSource', {
throw new ErrorPlainResponse('unableToGenerateIdFromResourceDataSource', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }
const idAttr = idAttrRaw as string; const idAttr = idAttrRaw as string;
@@ -186,28 +203,35 @@ export const handleCreateItem: Middleware = async (req) => {
params = { ...body as Record<string, unknown> }; params = { ...body as Record<string, unknown> };
params[idAttr] = newId; params[idAttr] = newId;
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToGenerateIdFromResourceDataSource', {
throw new ErrorPlainResponse('unableToGenerateIdFromResourceDataSource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


const location = `${basePath}/${resource.state.routeName}/${newId}`;
res.emit('response', {
Location: location,
});
// already return 202 accepted here

let newObject; let newObject;
let totalItemCount: number | undefined; let totalItemCount: number | undefined;

try { try {
newObject = await resource.dataSource.create(params); newObject = await resource.dataSource.create(params);
if (backend!.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { if (backend!.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') {
totalItemCount = await resource.dataSource.getTotalCount(); totalItemCount = await resource.dataSource.getTotalCount();
} }
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToCreateResource', {
throw new ErrorPlainResponse('unableToCreateResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


const location = `${basePath}/${resource.state.routeName}/${newId}`;

if (typeof totalItemCount !== 'undefined') { if (typeof totalItemCount !== 'undefined') {
return new PlainResponse({ return new PlainResponse({
statusCode: constants.HTTP_STATUS_CREATED, statusCode: constants.HTTP_STATUS_CREATED,
@@ -216,7 +240,8 @@ export const handleCreateItem: Middleware = async (req) => {
'X-Resource-Total-Item-Count': totalItemCount.toString() 'X-Resource-Total-Item-Count': totalItemCount.toString()
}, },
body: newObject, body: newObject,
statusMessage: 'resourceCreated'
statusMessage: 'resourceCreated',
res,
}); });
} }


@@ -226,17 +251,19 @@ export const handleCreateItem: Middleware = async (req) => {
headers: { headers: {
'Location': location, 'Location': location,
}, },
statusMessage: 'resourceCreated'
statusMessage: 'resourceCreated',
res,
}); });
} }


export const handleEmplaceItem: Middleware = async (req) => {
export const handleEmplaceItem: Middleware = async (req, res) => {
const { resource, resourceId, basePath, body, backend } = req; const { resource, resourceId, basePath, body, backend } = req;


const idAttrRaw = resource.state.shared.get('idAttr'); const idAttrRaw = resource.state.shared.get('idAttr');
if (typeof idAttrRaw === 'undefined') { if (typeof idAttrRaw === 'undefined') {
throw new HttpMiddlewareError('unableToGenerateIdFromResourceDataSource', {
throw new ErrorPlainResponse('unableToGenerateIdFromResourceDataSource', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }
const idAttr = idAttrRaw as string; const idAttr = idAttrRaw as string;
@@ -248,9 +275,10 @@ export const handleEmplaceItem: Middleware = async (req) => {
params[idAttr] = resourceId; params[idAttr] = resourceId;
[newObject, isCreated] = await resource.dataSource.emplace(resourceId!, params); [newObject, isCreated] = await resource.dataSource.emplace(resourceId!, params);
} catch (cause) { } catch (cause) {
throw new HttpMiddlewareError('unableToEmplaceResource', {
throw new ErrorPlainResponse('unableToEmplaceResource', {
cause, cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
}); });
} }


@@ -274,6 +302,7 @@ export const handleEmplaceItem: Middleware = async (req) => {
? 'resourceCreated' ? 'resourceCreated'
: 'resourceReplaced' : 'resourceReplaced'
), ),
body: newObject
body: newObject,
res,
}); });
} }

+ 14
- 5
src/backend/servers/http/response.ts View File

@@ -1,20 +1,25 @@
import {Language, LanguageStatusMessageMap} from '../../../common'; import {Language, LanguageStatusMessageMap} from '../../../common';
import {MiddlewareResponseError, Response} from '../../common'; import {MiddlewareResponseError, Response} from '../../common';


interface PlainResponseParams<T = unknown> extends Response {
interface PlainResponseParams<T = unknown, U extends NodeJS.EventEmitter = NodeJS.EventEmitter> extends Response {
body?: T; body?: T;
res: U;
} }


interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> {
interface HttpMiddlewareErrorParams<T = unknown, U extends NodeJS.EventEmitter = NodeJS.EventEmitter> extends Omit<PlainResponseParams<T, U>, 'statusMessage'> {
cause?: unknown cause?: unknown
} }


export class HttpMiddlewareError<T = unknown> extends MiddlewareResponseError implements PlainResponseParams<T> {
body?: T;
export class ErrorPlainResponse<T = unknown, U extends NodeJS.EventEmitter = NodeJS.EventEmitter> extends MiddlewareResponseError implements PlainResponseParams<T, U> {
readonly body?: T;
readonly res: U;


constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams<T>) {
constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams<T, U>) {
super(statusMessage, params); super(statusMessage, params);
this.body = params.body; this.body = params.body;
this.res = params.res;
this.res.emit('response', this);
this.res.emit('close');
} }
} }


@@ -32,5 +37,9 @@ export class PlainResponse<T = unknown> implements Response {
this.statusMessage = args.statusMessage; this.statusMessage = args.statusMessage;
this.headers = args.headers; this.headers = args.headers;
this.body = args.body; this.body = args.body;
args.res.emit('response', this);
args.res.emit('close');
} }
} }

// TODO stream response

+ 2
- 0
src/common/language.ts View File

@@ -34,6 +34,7 @@ export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [
'resourceCreated', 'resourceCreated',
'resourceReplaced', 'resourceReplaced',
'notImplemented', 'notImplemented',
'provideOptions',
] as const; ] as const;


export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number];
@@ -74,6 +75,7 @@ export const FALLBACK_LANGUAGE = {
urlNotFound: 'URL Not Found', urlNotFound: 'URL Not Found',
badRequest: 'Bad Request', badRequest: 'Bad Request',
ok: 'OK', ok: 'OK',
provideOptions: 'Provide Options',
resourceCollectionFetched: '$RESOURCE Collection Fetched', resourceCollectionFetched: '$RESOURCE Collection Fetched',
resourceFetched: '$RESOURCE Fetched', resourceFetched: '$RESOURCE Fetched',
resourceNotFound: '$RESOURCE Not Found', resourceNotFound: '$RESOURCE Not Found',


Loading…
Cancel
Save