From 1e8b169744a1cb30a4c29c13c5a673b13b808cb9 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Thu, 24 Aug 2023 17:33:23 +0200 Subject: [PATCH 1/9] #237: Implemented request model for running procedures --- packages/runtime/src/hooks/runtime.ts | 10 +++-- packages/runtime/src/interfaces/Middleware.ts | 4 +- packages/runtime/src/interfaces/Runner.ts | 4 +- packages/runtime/src/lib.ts | 1 + packages/runtime/src/models/Context.ts | 12 ------ packages/runtime/src/models/Request.ts | 36 ++++++++++++++++ packages/runtime/src/services/LocalGateway.ts | 11 ++--- packages/runtime/src/services/LocalNode.ts | 28 ++++++------- packages/runtime/src/services/NodeBalancer.ts | 8 ++-- .../runtime/src/services/ProcedureRunner.ts | 8 ++-- .../runtime/src/services/ProcedureRuntime.ts | 13 +++--- packages/runtime/src/services/Proxy.ts | 6 +-- packages/runtime/src/services/Remote.ts | 14 +++---- .../runtime/src/services/RemoteGateway.ts | 6 +-- packages/runtime/src/services/RemoteNode.ts | 6 +-- .../src/controllers/RPCController.ts | 41 ++++++++++--------- .../src/middleware/CorsMiddleware.ts | 14 +++---- 17 files changed, 125 insertions(+), 97 deletions(-) delete mode 100644 packages/runtime/src/models/Context.ts create mode 100644 packages/runtime/src/models/Request.ts diff --git a/packages/runtime/src/hooks/runtime.ts b/packages/runtime/src/hooks/runtime.ts index 1e26ecd5..d46c4686 100644 --- a/packages/runtime/src/hooks/runtime.ts +++ b/packages/runtime/src/hooks/runtime.ts @@ -1,7 +1,7 @@ import RuntimeNotAvailable from '../errors/RuntimeNotAvailable.js'; -import Context from '../models/Context.js'; +import Request from '../models/Request.js'; import VersionParser from '../utils/VersionParser.js'; import LocalNode from '../services/LocalNode.js'; @@ -13,7 +13,7 @@ export function setRuntime(runtime: LocalNode): void _runtime = runtime; } -export async function runProcedure(fqn: string, versionNumber: string, args: object, context?: object): Promise +export async function runProcedure(fqn: string, versionNumber: string, args: object, sourceRequest?: Request): Promise { if (_runtime === undefined) { @@ -22,7 +22,9 @@ export async function runProcedure(fqn: string, versionNumber: string, args: obj const version = VersionParser.parse(versionNumber); const argsMap = new Map(Object.entries(args)); - const headersMap = context instanceof Context ? context.headers : new Map(); + const headersMap = sourceRequest instanceof Request ? sourceRequest.headers : new Map(); - return _runtime.run(fqn, version, argsMap, headersMap); + const targetRequest = new Request(fqn, version, argsMap, headersMap); + + return _runtime.run(targetRequest); } diff --git a/packages/runtime/src/interfaces/Middleware.ts b/packages/runtime/src/interfaces/Middleware.ts index 82aca218..3ef566a5 100644 --- a/packages/runtime/src/interfaces/Middleware.ts +++ b/packages/runtime/src/interfaces/Middleware.ts @@ -1,11 +1,11 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import NextHandler from '../types/NextHandler.js'; interface Middleware { - handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise; + handle(request: Request, next: NextHandler): Promise; } export default Middleware; diff --git a/packages/runtime/src/interfaces/Runner.ts b/packages/runtime/src/interfaces/Runner.ts index 11852ffc..8e80b1e8 100644 --- a/packages/runtime/src/interfaces/Runner.ts +++ b/packages/runtime/src/interfaces/Runner.ts @@ -1,9 +1,9 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; interface Runner { - run(fqn: string, version: Version, args: Map, headers: Map): Promise; + run(request: Request): Promise; } export default Runner; diff --git a/packages/runtime/src/lib.ts b/packages/runtime/src/lib.ts index 7fba87aa..3308292f 100644 --- a/packages/runtime/src/lib.ts +++ b/packages/runtime/src/lib.ts @@ -44,6 +44,7 @@ export { default as Implementation } from './models/Implementation.js'; export { default as NamedParameter } from './models/NamedParameter.js'; export { default as ObjectParameter } from './models/ObjectParameter.js'; export { default as Procedure } from './models/Procedure.js'; +export { default as Request } from './models/Request.js'; export { default as Segment } from './models/Segment.js'; export { default as Version } from './models/Version.js'; diff --git a/packages/runtime/src/models/Context.ts b/packages/runtime/src/models/Context.ts deleted file mode 100644 index 40f2dc15..00000000 --- a/packages/runtime/src/models/Context.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export default class Context -{ - #headers: Map = new Map(); - - constructor(headers: Map) - { - this.#headers = headers; - } - - get headers() { return this.#headers; } -} diff --git a/packages/runtime/src/models/Request.ts b/packages/runtime/src/models/Request.ts new file mode 100644 index 00000000..0ec29fbb --- /dev/null +++ b/packages/runtime/src/models/Request.ts @@ -0,0 +1,36 @@ + +import Version from './Version.js'; + +export default class Request +{ + #fqn: string; + #version: Version; + #args: Map; + #headers: Map = new Map(); + + constructor(fqn: string, version: Version, args: Map, headers: Map) + { + this.#fqn = fqn; + this.#version = version; + this.#args = args; + this.#headers = headers; + } + + get fqn() { return this.#fqn; } + + get version() { return this.#version; } + + get args() { return this.#args; } + + get headers() { return this.#headers; } + + clearHeaders() + { + this.#headers.clear(); + } + + setHeader(name: string, value: string) + { + this.#headers.set(name, value); + } +} diff --git a/packages/runtime/src/services/LocalGateway.ts b/packages/runtime/src/services/LocalGateway.ts index c0494182..fe0836cf 100644 --- a/packages/runtime/src/services/LocalGateway.ts +++ b/packages/runtime/src/services/LocalGateway.ts @@ -1,7 +1,8 @@ import ProcedureNotFound from '../errors/ProcedureNotFound.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; + import ModuleLoader from '../utils/ModuleLoader.js'; import Gateway from './Gateway.js'; @@ -92,15 +93,15 @@ export default class LocalGateway extends Gateway return balancer; } - run(fqn: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { - const balancer = this.#getBalancer(fqn); + const balancer = this.#getBalancer(request.fqn); if (balancer === undefined) { - throw new ProcedureNotFound(fqn); + throw new ProcedureNotFound(request.fqn); } - return balancer.run(fqn, version, args, headers); + return balancer.run(request); } } diff --git a/packages/runtime/src/services/LocalNode.ts b/packages/runtime/src/services/LocalNode.ts index ebcec669..86aeb8a2 100644 --- a/packages/runtime/src/services/LocalNode.ts +++ b/packages/runtime/src/services/LocalNode.ts @@ -5,10 +5,9 @@ import ImplementationNotFound from '../errors/ImplementationNotFound.js'; import ProcedureNotFound from '../errors/ProcedureNotFound.js'; import RepositoryNotAvailable from '../errors/RepositoryNotAvailable.js'; -import Context from '../models/Context.js'; import Procedure from '../models/Procedure.js'; +import Request from '../models/Request.js'; import Segment from '../models/Segment.js'; -import Version from '../models/Version.js'; import Module from '../types/Module.js'; import ArgumentConstructor from '../utils/ArgumentConstructor.js'; import ModuleLoader from '../utils/ModuleLoader.js'; @@ -126,37 +125,36 @@ export default class LocalNode extends Node return this.#repository.importModule(this.#clientId, url); } - run(fqn: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { - const procedure = this.#getProcedure(fqn); + const procedure = this.#getProcedure(request.fqn); return procedure === undefined - ? this.#runGateway(fqn, version, args, headers) - : this.#runProcedure(procedure, version, args, headers); + ? this.#runGateway(request) + : this.#runProcedure(procedure, request); } - #runGateway(fqn: string, version: Version, args: Map, headers: Map): Promise + #runGateway(request: Request): Promise { if (this.#gateway === undefined) { - throw new ProcedureNotFound(fqn); + throw new ProcedureNotFound(request.fqn); } - return this.#gateway.run(fqn, version, args, headers); + return this.#gateway.run(request); } - #runProcedure(procedure: Procedure, version: Version, args: Map, headers: Map): Promise + #runProcedure(procedure: Procedure, request: Request): Promise { - const implementation = procedure.getImplementation(version); + const implementation = procedure.getImplementation(request.version); if (implementation === undefined) { - throw new ImplementationNotFound(procedure.fqn, version.toString()); + throw new ImplementationNotFound(procedure.fqn, request.version.toString()); } - const context = new Context(headers); - const values: unknown[] = this.#argumentConstructor.extract(implementation.parameters, args); + const values: unknown[] = this.#argumentConstructor.extract(implementation.parameters, request.args); - return implementation.executable.call(context, ...values); + return implementation.executable.call(request, ...values); } } diff --git a/packages/runtime/src/services/NodeBalancer.ts b/packages/runtime/src/services/NodeBalancer.ts index e5a002b5..d2e80141 100644 --- a/packages/runtime/src/services/NodeBalancer.ts +++ b/packages/runtime/src/services/NodeBalancer.ts @@ -1,7 +1,7 @@ import NoNodeAvailable from '../errors/NoNodeAvailable.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import Node from './Node.js'; @@ -47,15 +47,15 @@ export default class NodeBalancer return this.#nodes[this.#currentIndex++]; } - run(fqn: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { const node = this.getNextNode(); if (node === undefined) { - throw new NoNodeAvailable(fqn); + throw new NoNodeAvailable(request.fqn); } - return node.run(fqn, version, args, headers); + return node.run(request); } } diff --git a/packages/runtime/src/services/ProcedureRunner.ts b/packages/runtime/src/services/ProcedureRunner.ts index 30808d67..bfd725be 100644 --- a/packages/runtime/src/services/ProcedureRunner.ts +++ b/packages/runtime/src/services/ProcedureRunner.ts @@ -1,6 +1,6 @@ import Middleware from '../interfaces/Middleware.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import NextHandler from '../types/NextHandler.js'; import ProcedureRuntime from './ProcedureRuntime.js'; @@ -15,11 +15,11 @@ export default class ProcedureRunner implements Middleware } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { - const result = await this.#runner.run(fqn, version, args, headers); + const result = await this.#runner.run(request); - headers.clear(); + request.clearHeaders(); return result; } diff --git a/packages/runtime/src/services/ProcedureRuntime.ts b/packages/runtime/src/services/ProcedureRuntime.ts index dc0b7b93..192baac1 100644 --- a/packages/runtime/src/services/ProcedureRuntime.ts +++ b/packages/runtime/src/services/ProcedureRuntime.ts @@ -1,6 +1,7 @@ import Middleware from '../interfaces/Middleware.js'; import Runner from '../interfaces/Runner.js'; +import Request from '../models/Request.js'; import Version from '../models/Version.js'; import NextHandler from '../types/NextHandler.js'; @@ -22,7 +23,7 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner abstract hasProcedure(name: string): boolean; - abstract run(fqn: string, version: Version, args: Map, headers: Map): Promise; + abstract run(request: Request): Promise; addMiddleware(middleware: Middleware) { @@ -39,14 +40,14 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner return this.#middlewares.find(middleware => middleware instanceof type); } - handle(fqn: string, version: Version, args: Map, headers: Map): Promise + handle(request: Request): Promise { - const startHandler = this.#getNextHandler(fqn, version, args, headers, 0); + const startHandler = this.#getNextHandler(request, 0); return startHandler(); } - #getNextHandler(fqn: string, version: Version, args: Map, headers: Map, index: number): NextHandler + #getNextHandler(request: Request, index: number): NextHandler { const next = this.#middlewares[index]; @@ -56,8 +57,8 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner return async () => {}; } - const nextHandler = this.#getNextHandler(fqn, version, args, headers, index + 1); + const nextHandler = this.#getNextHandler(request, index + 1); - return async () => { return next.handle(fqn, version, args, headers, nextHandler); }; + return async () => { return next.handle(request, nextHandler); }; } } diff --git a/packages/runtime/src/services/Proxy.ts b/packages/runtime/src/services/Proxy.ts index 99c7443a..78e5b50d 100644 --- a/packages/runtime/src/services/Proxy.ts +++ b/packages/runtime/src/services/Proxy.ts @@ -1,6 +1,6 @@ import File from '../models/File.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import Repository from './Repository.js'; import ProcedureRuntime from './ProcedureRuntime.js'; @@ -49,8 +49,8 @@ export default class Proxy extends ProcedureRuntime return this.#repository.loadModule(clientId, filename); } - run(name: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { - return this.#runner.run(name, version, args, headers); + return this.#runner.run(request); } } diff --git a/packages/runtime/src/services/Remote.ts b/packages/runtime/src/services/Remote.ts index 97c635ec..7a76ebbe 100644 --- a/packages/runtime/src/services/Remote.ts +++ b/packages/runtime/src/services/Remote.ts @@ -2,7 +2,7 @@ import { Serializer, SerializerBuilder } from '@jitar/serialization'; import File from '../models/File.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import Module from '../types/Module.js'; import ModuleLoader from '../utils/ModuleLoader.js'; import RemoteClassLoader from '../utils/RemoteClassLoader.js'; @@ -98,15 +98,15 @@ export default class Remote await this.#callRemote(url, options, 201); } - async run(fqn: string, version: Version, args: Map, headers: Map): Promise + async run(request: Request): Promise { - headers.set('content-type', APPLICATION_JSON); + request.setHeader('content-type', APPLICATION_JSON); - const versionString = version.toString(); - const argsObject = Object.fromEntries(args); - const headersObject = Object.fromEntries(headers); + const versionString = request.version.toString(); + const argsObject = Object.fromEntries(request.args); + const headersObject = Object.fromEntries(request.headers); - const url = `${this.#url}/rpc/${fqn}?version=${versionString}&serialize=true`; + const url = `${this.#url}/rpc/${request.fqn}?version=${versionString}&serialize=true`; const body = await this.#createRequestBody(argsObject); const options = { diff --git a/packages/runtime/src/services/RemoteGateway.ts b/packages/runtime/src/services/RemoteGateway.ts index df5f0aaf..ec96e675 100644 --- a/packages/runtime/src/services/RemoteGateway.ts +++ b/packages/runtime/src/services/RemoteGateway.ts @@ -1,7 +1,7 @@ import NotImplemented from '../errors/generic/NotImplemented.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import Gateway from './Gateway.js'; import Node from './Node.js'; @@ -34,8 +34,8 @@ export default class RemoteGateway extends Gateway return this.#remote.addNode(node); } - run(fqn: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { - return this.#remote.run(fqn, version, args, headers); + return this.#remote.run(request); } } diff --git a/packages/runtime/src/services/RemoteNode.ts b/packages/runtime/src/services/RemoteNode.ts index 32df0ca8..0e3ca5bf 100644 --- a/packages/runtime/src/services/RemoteNode.ts +++ b/packages/runtime/src/services/RemoteNode.ts @@ -1,5 +1,5 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; import Node from './Node.js'; import Remote from './Remote.js'; @@ -43,8 +43,8 @@ export default class RemoteNode extends Node return this.#remote.getHealth(); } - run(fqn: string, version: Version, args: Map, headers: Map): Promise + run(request: Request): Promise { - return this.#remote.run(fqn, version, args, headers); + return this.#remote.run(request); } } diff --git a/packages/server-nodejs/src/controllers/RPCController.ts b/packages/server-nodejs/src/controllers/RPCController.ts index 9c0e9b35..fcb1e752 100644 --- a/packages/server-nodejs/src/controllers/RPCController.ts +++ b/packages/server-nodejs/src/controllers/RPCController.ts @@ -1,8 +1,8 @@ -import express, { Request, Response } from 'express'; +import express, { Request as ExpressRequest, Response as ExpressResponse } from 'express'; import { Logger } from 'tslog'; -import { Version, VersionParser, ProcedureRuntime, BadRequest, Unauthorized, PaymentRequired, Forbidden, NotFound, Teapot, NotImplemented } from '@jitar/runtime'; +import { Request as JitarRequest, Version, VersionParser, ProcedureRuntime, BadRequest, Unauthorized, PaymentRequired, Forbidden, NotFound, Teapot, NotImplemented } from '@jitar/runtime'; import { Serializer } from '@jitar/serialization'; import CorsMiddleware from '../middleware/CorsMiddleware.js'; @@ -32,9 +32,9 @@ export default class RPCController this.#serializer = serializer; this.#logger = logger; - app.get('/rpc/*', (request: Request, response: Response) => { this.runGet(request, response); }); - app.post('/rpc/*', (request: Request, response: Response) => { this.runPost(request, response); }); - app.options('/rpc/*', (request: Request, response: Response) => { this.runOptions(request, response); }); + app.get('/rpc/*', (request: ExpressRequest, response: ExpressResponse) => { this.runGet(request, response); }); + app.post('/rpc/*', (request: ExpressRequest, response: ExpressResponse) => { this.runPost(request, response); }); + app.options('/rpc/*', (request: ExpressRequest, response: ExpressResponse) => { this.runOptions(request, response); }); this.#showProcedureInfo(); } @@ -53,7 +53,7 @@ export default class RPCController this.#logger.info('Registered RPC entries', procedureNames); } - async runGet(request: Request, response: Response): Promise + async runGet(request: ExpressRequest, response: ExpressResponse): Promise { const fqn = this.#extractFqn(request); const version = this.#extractVersion(request); @@ -64,7 +64,7 @@ export default class RPCController return this.#run(fqn, version, args, headers, response, serialize); } - async runPost(request: Request, response: Response): Promise + async runPost(request: ExpressRequest, response: ExpressResponse): Promise { const fqn = this.#extractFqn(request); const version = this.#extractVersion(request); @@ -75,29 +75,29 @@ export default class RPCController return this.#run(fqn, version, args, headers, response, serialize); } - async runOptions(request: Request, response: Response): Promise + async runOptions(request: ExpressRequest, response: ExpressResponse): Promise { return this.#setCors(response); } - #extractFqn(request: Request): string + #extractFqn(request: ExpressRequest): string { return request.path.substring(5); } - #extractVersion(request: Request): Version + #extractVersion(request: ExpressRequest): Version { return request.query.version !== undefined ? VersionParser.parse(request.query.version.toString()) : Version.DEFAULT; } - #extractSerialize(request: Request): boolean + #extractSerialize(request: ExpressRequest): boolean { return request.query.serialize === 'true'; } - #extractQueryArguments(request: Request): Record + #extractQueryArguments(request: ExpressRequest): Record { const args: Record = {}; @@ -117,12 +117,12 @@ export default class RPCController return args; } - #extractBodyArguments(request: Request): Record + #extractBodyArguments(request: ExpressRequest): Record { return request.body; } - #extractHeaders(request: Request): Map + #extractHeaders(request: ExpressRequest): Map { const headers = new Map(); @@ -147,7 +147,7 @@ export default class RPCController return headers; } - async #run(fqn: string, version: Version, args: Record, headers: Map, response: Response, serialize: boolean): Promise + async #run(fqn: string, version: Version, args: Record, headers: Map, response: ExpressResponse, serialize: boolean): Promise { if (this.#runtime.hasProcedure(fqn) === false) { @@ -160,7 +160,8 @@ export default class RPCController const deserializedArgs = await this.#serializer.deserialize(args) as Record; const argsMap = new Map(Object.entries(deserializedArgs)); - const result = await this.#runtime.handle(fqn, version, argsMap, headers); + const request = new JitarRequest(fqn, version, argsMap, headers); + const result = await this.#runtime.handle(request); this.#logger.info(`Ran procedure -> ${fqn} (v${version.toString()})`); @@ -182,7 +183,7 @@ export default class RPCController } } - async #setCors(response: Response): Promise + async #setCors(response: ExpressResponse): Promise { const cors = this.#runtime.getMiddleware(CorsMiddleware) as CorsMiddleware; @@ -199,7 +200,7 @@ export default class RPCController return response.status(204).send(); } - async #createResultResponse(result: unknown, response: Response, serialize: boolean): Promise + async #createResultResponse(result: unknown, response: ExpressResponse, serialize: boolean): Promise { const content = await this.#createResponseContent(result, serialize); const contentType = this.#createResponseContentType(content); @@ -210,7 +211,7 @@ export default class RPCController return response.status(200).send(responseContent); } - async #createErrorResponse(error: unknown, errorData: unknown, response: Response, serialize: boolean): Promise + async #createErrorResponse(error: unknown, errorData: unknown, response: ExpressResponse, serialize: boolean): Promise { const content = await this.#createResponseContent(errorData, serialize); const contentType = this.#createResponseContentType(content); @@ -235,7 +236,7 @@ export default class RPCController : 'text/plain'; } - #setResponseHeaders(response: Response, headers: Map): void + #setResponseHeaders(response: ExpressResponse, headers: Map): void { headers.forEach((value, key) => response.setHeader(key, value)); } diff --git a/packages/server-nodejs/src/middleware/CorsMiddleware.ts b/packages/server-nodejs/src/middleware/CorsMiddleware.ts index afc2d85f..b76de7f3 100644 --- a/packages/server-nodejs/src/middleware/CorsMiddleware.ts +++ b/packages/server-nodejs/src/middleware/CorsMiddleware.ts @@ -1,5 +1,5 @@ -import { Middleware, NextHandler, Version } from '@jitar/runtime'; +import { Middleware, NextHandler, Request } from '@jitar/runtime'; export default class CorsMiddleware implements Middleware { @@ -19,19 +19,19 @@ export default class CorsMiddleware implements Middleware get allowHeaders() { return this.#allowHeaders; } - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { const result = await next(); - this.#setHeaders(headers); + this.#setHeaders(request); return result; } - #setHeaders(headers: Map): void + #setHeaders(request: Request): void { - headers.set('Access-Control-Allow-Origin', this.#allowOrigin); - headers.set('Access-Control-Allow-Methods', this.#allowMethods); - headers.set('Access-Control-Allow-Headers', this.#allowHeaders); + request.setHeader('Access-Control-Allow-Origin', this.#allowOrigin); + request.setHeader('Access-Control-Allow-Methods', this.#allowMethods); + request.setHeader('Access-Control-Allow-Headers', this.#allowHeaders); } } From ba2295251f435fda3d7a09cfa2ff23d0cfdd8c9a Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Fri, 25 Aug 2023 16:17:46 +0200 Subject: [PATCH 2/9] #237: Refactored tests to the new request model --- packages/runtime/src/models/Request.ts | 25 +++++++++++++++++++ .../interfaces/Middleware.fixture.ts | 20 +++++++-------- .../test/services/LocalGateway.spec.ts | 10 +++++--- .../runtime/test/services/LocalNode.spec.ts | 13 +++++++--- .../test/services/NodeBalancer.spec.ts | 4 ++- .../test/services/ProcedureRuntime.spec.ts | 4 ++- 6 files changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/runtime/src/models/Request.ts b/packages/runtime/src/models/Request.ts index 0ec29fbb..98acde4c 100644 --- a/packages/runtime/src/models/Request.ts +++ b/packages/runtime/src/models/Request.ts @@ -24,6 +24,21 @@ export default class Request get headers() { return this.#headers; } + setArgument(name: string, value: unknown) + { + this.#args.set(name, value); + } + + getArgument(name: string): unknown + { + return this.#args.get(name); + } + + removeArgument(name: string) + { + this.#args.delete(name); + } + clearHeaders() { this.#headers.clear(); @@ -33,4 +48,14 @@ export default class Request { this.#headers.set(name, value); } + + getHeader(name: string): string | undefined + { + return this.#headers.get(name); + } + + removeHeader(name: string) + { + this.#headers.delete(name); + } } diff --git a/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts b/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts index 4ef0c1ee..055f2fd8 100644 --- a/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts +++ b/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts @@ -1,14 +1,14 @@ -import Version from '../../../src/models/Version'; +import Request from '../../../src/models/Request'; import Middleware from '../../../src/interfaces/Middleware'; class FirstMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(fqn: string, version: Version, args: Map, headers: Map, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { - headers.set('first', 'yes'); - headers.set('last', '1'); + request.setHeader('first', 'yes'); + request.setHeader('last', '1'); const result = await next(); @@ -19,10 +19,10 @@ class FirstMiddleware implements Middleware class SecondMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(fqn: string, version: Version, args: Map, headers: Map, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { - headers.set('second', 'yes'); - headers.set('last', '2'); + request.setHeader('second', 'yes'); + request.setHeader('last', '2'); const result = await next(); @@ -33,10 +33,10 @@ class SecondMiddleware implements Middleware class ThirdMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(fqn: string, version: Version, args: Map, headers: Map, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { - headers.set('third', 'yes'); - headers.set('last', '3'); + request.setHeader('third', 'yes'); + request.setHeader('last', '3'); return '3'; } diff --git a/packages/runtime/test/services/LocalGateway.spec.ts b/packages/runtime/test/services/LocalGateway.spec.ts index e4fa36fc..fa616e68 100644 --- a/packages/runtime/test/services/LocalGateway.spec.ts +++ b/packages/runtime/test/services/LocalGateway.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import ProcedureNotFound from '../../src/errors/ProcedureNotFound'; +import Request from '../../src/models/Request'; import Version from '../../src/models/Version'; import { GATEWAYS, GATEWAY_URL } from '../_fixtures/services/LocalGateway.fixture'; @@ -55,21 +56,24 @@ describe('services/LocalGateway', () => { it('should find and run a procedure from a node', async () => { - const firstResult = await gateway.run('second', Version.DEFAULT, new Map(), new Map()); + const request = new Request('second', Version.DEFAULT, new Map(), new Map()); + const firstResult = await gateway.run(request); expect(firstResult).toBe('first'); }); it('should find and run a procedure from a node that calls a procedure on another node', async () => { - const result = await gateway.run('third', Version.DEFAULT, new Map(), new Map()); + const request = new Request('third', Version.DEFAULT, new Map(), new Map()); + const result = await gateway.run(request); expect(result).toBe('fourth'); }); it('should not run a non-existing procedure', async () => { - const run = async () => gateway.run('nonExisting', Version.DEFAULT, new Map(), new Map()); + const request = new Request('nonExisting', Version.DEFAULT, new Map(), new Map()); + const run = async () => gateway.run(request); expect(run).rejects.toEqual(new ProcedureNotFound('nonExisting')); }); diff --git a/packages/runtime/test/services/LocalNode.spec.ts b/packages/runtime/test/services/LocalNode.spec.ts index ff0dae39..7b70cb92 100644 --- a/packages/runtime/test/services/LocalNode.spec.ts +++ b/packages/runtime/test/services/LocalNode.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import ProcedureNotFound from '../../src/errors/ProcedureNotFound'; +import Request from '../../src/models/Request'; import Version from '../../src/models/Version'; import { NODES } from '../_fixtures/services/LocalNode.fixture'; @@ -52,28 +53,32 @@ describe('services/LocalNode', () => { it('should run a public procedure that calls a private procedure on the same segment', async () => { - const result = await node.run('second', Version.DEFAULT, new Map(), new Map()); + const request = new Request('second', Version.DEFAULT, new Map(), new Map()); + const result = await node.run(request); expect(result).toBe('first'); }); it('should run a public procedure that calls a private procedure on another segment', async () => { - const result = await node.run('sixth', Version.DEFAULT, new Map(), new Map()); + const request = new Request('sixth', Version.DEFAULT, new Map(), new Map()); + const result = await node.run(request); expect(result).toBe('first'); }); it('should run a public procedure that calls a public procedure on another segment', async () => { - const result = await node.run('third', Version.DEFAULT, new Map(), new Map()); + const request = new Request('third', Version.DEFAULT, new Map(), new Map()); + const result = await node.run(request); expect(result).toBe('fourth'); }); it('should not run a non-existing procedure', async () => { - const run = async () => node.run('nonExisting', Version.DEFAULT, new Map(), new Map()); + const request = new Request('nonExisting', Version.DEFAULT, new Map(), new Map()); + const run = async () => node.run(request); expect(run).rejects.toEqual(new ProcedureNotFound('nonExisting')); }); diff --git a/packages/runtime/test/services/NodeBalancer.spec.ts b/packages/runtime/test/services/NodeBalancer.spec.ts index 96350ac4..74f835e4 100644 --- a/packages/runtime/test/services/NodeBalancer.spec.ts +++ b/packages/runtime/test/services/NodeBalancer.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import NoNodeAvailable from '../../src/errors/NoNodeAvailable'; +import Request from '../../src/models/Request'; import Version from '../../src/models/Version'; import { BALANCERS, NODES } from '../_fixtures/services/NodeBalancer.fixture'; @@ -31,7 +32,8 @@ describe('services/LocalGateway', () => { it('should throw a node not available error', async () => { - const run = async () => emptyBalancer.run('nonExisting', Version.DEFAULT, new Map(), new Map()); + const request = new Request('nonExisting', Version.DEFAULT, new Map(), new Map()); + const run = async () => emptyBalancer.run(request); expect(run).rejects.toEqual(new NoNodeAvailable('nonExisting')); }); diff --git a/packages/runtime/test/services/ProcedureRuntime.spec.ts b/packages/runtime/test/services/ProcedureRuntime.spec.ts index 231df340..9ab8f466 100644 --- a/packages/runtime/test/services/ProcedureRuntime.spec.ts +++ b/packages/runtime/test/services/ProcedureRuntime.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; +import Request from '../../src/models/Request'; import Version from '../../src/models/Version'; import { RUNTIMES } from '../_fixtures/services/ProcedureRuntime.fixture'; @@ -16,7 +17,8 @@ describe('services/ProcedureRuntime', () => const args = new Map(); const headers = new Map(); - const result = await runtime.handle('test', new Version(1, 0, 0), args, headers); + const request = new Request('test', new Version(1, 0, 0), args, headers); + const result = await runtime.handle(request); expect(result).toBe('123'); expect(headers.get('first')).toBe('yes'); From c4f8f08f4175efb492729620191181e780286e13 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Fri, 25 Aug 2023 16:18:20 +0200 Subject: [PATCH 3/9] #237: Updated the middleware example --- examples/concepts/middleware/src/LoggingMiddleware.ts | 8 ++++---- packages/jitar/src/client.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/concepts/middleware/src/LoggingMiddleware.ts b/examples/concepts/middleware/src/LoggingMiddleware.ts index 52e0b051..9f46bc1e 100644 --- a/examples/concepts/middleware/src/LoggingMiddleware.ts +++ b/examples/concepts/middleware/src/LoggingMiddleware.ts @@ -6,19 +6,19 @@ * Middleware is executed in the reversed order it is registered. */ -import { Middleware, Version, NextHandler } from 'jitar'; +import { Middleware, Request, NextHandler } from 'jitar'; export default class LoggingMiddleware implements Middleware { - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { - // Modify the request here + // Modify the request here (e.g. add a header) const result = await next(); // Modify the response (result) here - console.log(`Logging result for ${fqn} --> ${result}`); + console.log(`Logging result for ${request.fqn} --> ${result}`); return result; } diff --git a/packages/jitar/src/client.ts b/packages/jitar/src/client.ts index e51e69f0..b1614061 100644 --- a/packages/jitar/src/client.ts +++ b/packages/jitar/src/client.ts @@ -4,6 +4,7 @@ export HealthCheck, Middleware, NextHandler, + Request, Segment, Procedure, Implementation, From f1fc6e1e971123476f282d2fb8b5f3f629947ff8 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Fri, 25 Aug 2023 16:21:23 +0200 Subject: [PATCH 4/9] #237: Updated the middleware documentation --- documentation/docs/develop/middleware.md | 4 ++-- documentation/docs/develop/security.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/documentation/docs/develop/middleware.md b/documentation/docs/develop/middleware.md index 144b270e..97b451c0 100644 --- a/documentation/docs/develop/middleware.md +++ b/documentation/docs/develop/middleware.md @@ -24,11 +24,11 @@ Any middleware required to implement Jitars Middleware interface. This interface ```ts // src/MyMiddleware.ts -import { Middleware, Version, NextHandler } from 'jitar'; +import { Middleware, Request, NextHandler } from 'jitar'; export default class MyMiddleware implements Middleware { - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Modify the request (args and headers) here diff --git a/documentation/docs/develop/security.md b/documentation/docs/develop/security.md index 8d8aab6a..d14353e0 100644 --- a/documentation/docs/develop/security.md +++ b/documentation/docs/develop/security.md @@ -44,11 +44,11 @@ To prevent the access of server modules from any client, make sure that all modu The easiest way to add auth to your application is [using middleware](./middleware). We recommend separating the implementation of the authentication and authorization process. This enables allocating these tasks to different services in a distributed setup. Our typical setup looks like this. ```ts -import { Middleware, Version, NextHandler } from 'jitar'; +import { Middleware, Request, NextHandler } from 'jitar'; export default class Authentication implements Middleware { - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Get Authorization header // Authenticate the user @@ -60,11 +60,11 @@ export default class Authentication implements Middleware In a distributed setup we register this middleware at the [gateway service](../fundamentals/runtime-services.md#gateway) to make sure a node only gets called when the user is authenticated. The authorization may depend on attributes gathered during the execution of the function. Therefore we add the authorization middleware to the [node service](../fundamentals/runtime-services#node). ```ts -import { Middleware, Version, NextHandler } from 'jitar'; +import { Middleware, Request, NextHandler } from 'jitar'; export default class Authorization implements Middleware { - async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Get user info from the args (remove if needed) // Authorize the user (RBAC, ABAC, …) From e18d3b427b431a7e2f5c17dc067404f3a4412635 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 29 Aug 2023 13:57:02 +0200 Subject: [PATCH 5/9] #237: Implemented response model for running procedures --- documentation/docs/develop/middleware.md | 8 ++-- documentation/docs/develop/security.md | 8 ++-- .../middleware/src/LoggingMiddleware.ts | 4 +- packages/jitar/src/client.ts | 1 + packages/runtime/src/hooks/runtime.ts | 3 +- packages/runtime/src/interfaces/Middleware.ts | 3 +- packages/runtime/src/interfaces/Runner.ts | 3 +- packages/runtime/src/lib.ts | 1 + packages/runtime/src/models/Response.ts | 38 +++++++++++++++++++ packages/runtime/src/services/LocalGateway.ts | 3 +- packages/runtime/src/services/LocalNode.ts | 11 ++++-- packages/runtime/src/services/NodeBalancer.ts | 3 +- .../runtime/src/services/ProcedureRunner.ts | 3 +- .../runtime/src/services/ProcedureRuntime.ts | 11 +++--- packages/runtime/src/services/Proxy.ts | 3 +- packages/runtime/src/services/Remote.ts | 8 +++- .../runtime/src/services/RemoteGateway.ts | 3 +- packages/runtime/src/services/RemoteNode.ts | 3 +- packages/runtime/src/types/NextHandler.ts | 4 +- .../interfaces/Middleware.fixture.ts | 19 ++++++---- .../test/services/LocalGateway.spec.ts | 8 ++-- .../runtime/test/services/LocalNode.spec.ts | 12 +++--- .../test/services/ProcedureRuntime.spec.ts | 4 +- .../src/controllers/RPCController.ts | 8 ++-- .../src/middleware/CorsMiddleware.ts | 18 ++++----- 25 files changed, 126 insertions(+), 64 deletions(-) create mode 100644 packages/runtime/src/models/Response.ts diff --git a/documentation/docs/develop/middleware.md b/documentation/docs/develop/middleware.md index 97b451c0..f9c8337c 100644 --- a/documentation/docs/develop/middleware.md +++ b/documentation/docs/develop/middleware.md @@ -24,19 +24,19 @@ Any middleware required to implement Jitars Middleware interface. This interface ```ts // src/MyMiddleware.ts -import { Middleware, Request, NextHandler } from 'jitar'; +import { Middleware, Request, Response, NextHandler } from 'jitar'; export default class MyMiddleware implements Middleware { - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Modify the request (args and headers) here - const result = await next(); + const response = await next(); // Modify the response (result) here - return result; + return response; } } ``` diff --git a/documentation/docs/develop/security.md b/documentation/docs/develop/security.md index d14353e0..03d9d987 100644 --- a/documentation/docs/develop/security.md +++ b/documentation/docs/develop/security.md @@ -44,11 +44,11 @@ To prevent the access of server modules from any client, make sure that all modu The easiest way to add auth to your application is [using middleware](./middleware). We recommend separating the implementation of the authentication and authorization process. This enables allocating these tasks to different services in a distributed setup. Our typical setup looks like this. ```ts -import { Middleware, Request, NextHandler } from 'jitar'; +import { Middleware, Request, Response, NextHandler } from 'jitar'; export default class Authentication implements Middleware { - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Get Authorization header // Authenticate the user @@ -60,11 +60,11 @@ export default class Authentication implements Middleware In a distributed setup we register this middleware at the [gateway service](../fundamentals/runtime-services.md#gateway) to make sure a node only gets called when the user is authenticated. The authorization may depend on attributes gathered during the execution of the function. Therefore we add the authorization middleware to the [node service](../fundamentals/runtime-services#node). ```ts -import { Middleware, Request, NextHandler } from 'jitar'; +import { Middleware, Request, Response, NextHandler } from 'jitar'; export default class Authorization implements Middleware { - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Get user info from the args (remove if needed) // Authorize the user (RBAC, ABAC, …) diff --git a/examples/concepts/middleware/src/LoggingMiddleware.ts b/examples/concepts/middleware/src/LoggingMiddleware.ts index 9f46bc1e..64f66b6a 100644 --- a/examples/concepts/middleware/src/LoggingMiddleware.ts +++ b/examples/concepts/middleware/src/LoggingMiddleware.ts @@ -6,11 +6,11 @@ * Middleware is executed in the reversed order it is registered. */ -import { Middleware, Request, NextHandler } from 'jitar'; +import { Middleware, Request, Response, NextHandler } from 'jitar'; export default class LoggingMiddleware implements Middleware { - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { // Modify the request here (e.g. add a header) diff --git a/packages/jitar/src/client.ts b/packages/jitar/src/client.ts index b1614061..da3c725b 100644 --- a/packages/jitar/src/client.ts +++ b/packages/jitar/src/client.ts @@ -5,6 +5,7 @@ export Middleware, NextHandler, Request, + Response, Segment, Procedure, Implementation, diff --git a/packages/runtime/src/hooks/runtime.ts b/packages/runtime/src/hooks/runtime.ts index d46c4686..9e63251c 100644 --- a/packages/runtime/src/hooks/runtime.ts +++ b/packages/runtime/src/hooks/runtime.ts @@ -25,6 +25,7 @@ export async function runProcedure(fqn: string, versionNumber: string, args: obj const headersMap = sourceRequest instanceof Request ? sourceRequest.headers : new Map(); const targetRequest = new Request(fqn, version, argsMap, headersMap); + const targetResponse = await _runtime.run(targetRequest); - return _runtime.run(targetRequest); + return targetResponse.result; } diff --git a/packages/runtime/src/interfaces/Middleware.ts b/packages/runtime/src/interfaces/Middleware.ts index 3ef566a5..ea5c0b3d 100644 --- a/packages/runtime/src/interfaces/Middleware.ts +++ b/packages/runtime/src/interfaces/Middleware.ts @@ -1,11 +1,12 @@ import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import NextHandler from '../types/NextHandler.js'; interface Middleware { - handle(request: Request, next: NextHandler): Promise; + handle(request: Request, next: NextHandler): Promise; } export default Middleware; diff --git a/packages/runtime/src/interfaces/Runner.ts b/packages/runtime/src/interfaces/Runner.ts index 8e80b1e8..8387c886 100644 --- a/packages/runtime/src/interfaces/Runner.ts +++ b/packages/runtime/src/interfaces/Runner.ts @@ -1,9 +1,10 @@ import Request from '../models/Request.js'; +import Response from '../models/Response.js'; interface Runner { - run(request: Request): Promise; + run(request: Request): Promise; } export default Runner; diff --git a/packages/runtime/src/lib.ts b/packages/runtime/src/lib.ts index 3308292f..a3c16eb5 100644 --- a/packages/runtime/src/lib.ts +++ b/packages/runtime/src/lib.ts @@ -45,6 +45,7 @@ export { default as NamedParameter } from './models/NamedParameter.js'; export { default as ObjectParameter } from './models/ObjectParameter.js'; export { default as Procedure } from './models/Procedure.js'; export { default as Request } from './models/Request.js'; +export { default as Response } from './models/Response.js'; export { default as Segment } from './models/Segment.js'; export { default as Version } from './models/Version.js'; diff --git a/packages/runtime/src/models/Response.ts b/packages/runtime/src/models/Response.ts new file mode 100644 index 00000000..2d217509 --- /dev/null +++ b/packages/runtime/src/models/Response.ts @@ -0,0 +1,38 @@ + +export default class Response +{ + #result: unknown; + #headers: Map; + + constructor(result: unknown = undefined, headers = new Map()) + { + this.#result = result; + this.#headers = headers; + } + + get result() { return this.#result; } + + set result(value: unknown) { this.#result = value; } + + get headers() { return this.#headers; } + + clearHeaders() + { + this.#headers.clear(); + } + + setHeader(name: string, value: string) + { + this.#headers.set(name, value); + } + + getHeader(name: string): string | undefined + { + return this.#headers.get(name); + } + + removeHeader(name: string) + { + this.#headers.delete(name); + } +} diff --git a/packages/runtime/src/services/LocalGateway.ts b/packages/runtime/src/services/LocalGateway.ts index fe0836cf..7e8b8355 100644 --- a/packages/runtime/src/services/LocalGateway.ts +++ b/packages/runtime/src/services/LocalGateway.ts @@ -2,6 +2,7 @@ import ProcedureNotFound from '../errors/ProcedureNotFound.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import ModuleLoader from '../utils/ModuleLoader.js'; @@ -93,7 +94,7 @@ export default class LocalGateway extends Gateway return balancer; } - run(request: Request): Promise + run(request: Request): Promise { const balancer = this.#getBalancer(request.fqn); diff --git a/packages/runtime/src/services/LocalNode.ts b/packages/runtime/src/services/LocalNode.ts index 86aeb8a2..e0c460b9 100644 --- a/packages/runtime/src/services/LocalNode.ts +++ b/packages/runtime/src/services/LocalNode.ts @@ -7,6 +7,7 @@ import RepositoryNotAvailable from '../errors/RepositoryNotAvailable.js'; import Procedure from '../models/Procedure.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Segment from '../models/Segment.js'; import Module from '../types/Module.js'; import ArgumentConstructor from '../utils/ArgumentConstructor.js'; @@ -125,7 +126,7 @@ export default class LocalNode extends Node return this.#repository.importModule(this.#clientId, url); } - run(request: Request): Promise + run(request: Request): Promise { const procedure = this.#getProcedure(request.fqn); @@ -134,7 +135,7 @@ export default class LocalNode extends Node : this.#runProcedure(procedure, request); } - #runGateway(request: Request): Promise + #runGateway(request: Request): Promise { if (this.#gateway === undefined) { @@ -144,7 +145,7 @@ export default class LocalNode extends Node return this.#gateway.run(request); } - #runProcedure(procedure: Procedure, request: Request): Promise + async #runProcedure(procedure: Procedure, request: Request): Promise { const implementation = procedure.getImplementation(request.version); @@ -155,6 +156,8 @@ export default class LocalNode extends Node const values: unknown[] = this.#argumentConstructor.extract(implementation.parameters, request.args); - return implementation.executable.call(request, ...values); + const result = await implementation.executable.call(request, ...values); + + return new Response(result); } } diff --git a/packages/runtime/src/services/NodeBalancer.ts b/packages/runtime/src/services/NodeBalancer.ts index d2e80141..b26131d0 100644 --- a/packages/runtime/src/services/NodeBalancer.ts +++ b/packages/runtime/src/services/NodeBalancer.ts @@ -2,6 +2,7 @@ import NoNodeAvailable from '../errors/NoNodeAvailable.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Node from './Node.js'; @@ -47,7 +48,7 @@ export default class NodeBalancer return this.#nodes[this.#currentIndex++]; } - run(request: Request): Promise + run(request: Request): Promise { const node = this.getNextNode(); diff --git a/packages/runtime/src/services/ProcedureRunner.ts b/packages/runtime/src/services/ProcedureRunner.ts index bfd725be..c73c02d1 100644 --- a/packages/runtime/src/services/ProcedureRunner.ts +++ b/packages/runtime/src/services/ProcedureRunner.ts @@ -1,6 +1,7 @@ import Middleware from '../interfaces/Middleware.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import NextHandler from '../types/NextHandler.js'; import ProcedureRuntime from './ProcedureRuntime.js'; @@ -15,7 +16,7 @@ export default class ProcedureRunner implements Middleware } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { const result = await this.#runner.run(request); diff --git a/packages/runtime/src/services/ProcedureRuntime.ts b/packages/runtime/src/services/ProcedureRuntime.ts index 192baac1..73032c36 100644 --- a/packages/runtime/src/services/ProcedureRuntime.ts +++ b/packages/runtime/src/services/ProcedureRuntime.ts @@ -1,8 +1,10 @@ import Middleware from '../interfaces/Middleware.js'; import Runner from '../interfaces/Runner.js'; + import Request from '../models/Request.js'; -import Version from '../models/Version.js'; +import Response from '../models/Response.js'; + import NextHandler from '../types/NextHandler.js'; import Runtime from './Runtime.js'; @@ -23,7 +25,7 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner abstract hasProcedure(name: string): boolean; - abstract run(request: Request): Promise; + abstract run(request: Request): Promise; addMiddleware(middleware: Middleware) { @@ -40,7 +42,7 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner return this.#middlewares.find(middleware => middleware instanceof type); } - handle(request: Request): Promise + handle(request: Request): Promise { const startHandler = this.#getNextHandler(request, 0); @@ -53,8 +55,7 @@ export default abstract class ProcedureRuntime extends Runtime implements Runner if (next === undefined) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return async () => {}; + return async () => new Response(); } const nextHandler = this.#getNextHandler(request, index + 1); diff --git a/packages/runtime/src/services/Proxy.ts b/packages/runtime/src/services/Proxy.ts index 78e5b50d..cd8dce99 100644 --- a/packages/runtime/src/services/Proxy.ts +++ b/packages/runtime/src/services/Proxy.ts @@ -1,6 +1,7 @@ import File from '../models/File.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Repository from './Repository.js'; import ProcedureRuntime from './ProcedureRuntime.js'; @@ -49,7 +50,7 @@ export default class Proxy extends ProcedureRuntime return this.#repository.loadModule(clientId, filename); } - run(request: Request): Promise + run(request: Request): Promise { return this.#runner.run(request); } diff --git a/packages/runtime/src/services/Remote.ts b/packages/runtime/src/services/Remote.ts index 7a76ebbe..91d62545 100644 --- a/packages/runtime/src/services/Remote.ts +++ b/packages/runtime/src/services/Remote.ts @@ -3,7 +3,10 @@ import { Serializer, SerializerBuilder } from '@jitar/serialization'; import File from '../models/File.js'; import Request from '../models/Request.js'; +import { default as ResultResponse } from '../models/Response.js'; + import Module from '../types/Module.js'; + import ModuleLoader from '../utils/ModuleLoader.js'; import RemoteClassLoader from '../utils/RemoteClassLoader.js'; @@ -98,7 +101,7 @@ export default class Remote await this.#callRemote(url, options, 201); } - async run(request: Request): Promise + async run(request: Request): Promise { request.setHeader('content-type', APPLICATION_JSON); @@ -116,8 +119,9 @@ export default class Remote }; const response = await this.#callRemote(url, options, 200); + const result = await this.#createResponseResult(response); - return this.#createResponseResult(response); + return new ResultResponse(result); } async #callRemote(url: string, options: object, expectedStatus: number): Promise diff --git a/packages/runtime/src/services/RemoteGateway.ts b/packages/runtime/src/services/RemoteGateway.ts index ec96e675..2ffd1f3c 100644 --- a/packages/runtime/src/services/RemoteGateway.ts +++ b/packages/runtime/src/services/RemoteGateway.ts @@ -2,6 +2,7 @@ import NotImplemented from '../errors/generic/NotImplemented.js'; import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Gateway from './Gateway.js'; import Node from './Node.js'; @@ -34,7 +35,7 @@ export default class RemoteGateway extends Gateway return this.#remote.addNode(node); } - run(request: Request): Promise + run(request: Request): Promise { return this.#remote.run(request); } diff --git a/packages/runtime/src/services/RemoteNode.ts b/packages/runtime/src/services/RemoteNode.ts index 0e3ca5bf..17b210fc 100644 --- a/packages/runtime/src/services/RemoteNode.ts +++ b/packages/runtime/src/services/RemoteNode.ts @@ -1,5 +1,6 @@ import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Node from './Node.js'; import Remote from './Remote.js'; @@ -43,7 +44,7 @@ export default class RemoteNode extends Node return this.#remote.getHealth(); } - run(request: Request): Promise + run(request: Request): Promise { return this.#remote.run(request); } diff --git a/packages/runtime/src/types/NextHandler.ts b/packages/runtime/src/types/NextHandler.ts index 35195bed..3c07d95b 100644 --- a/packages/runtime/src/types/NextHandler.ts +++ b/packages/runtime/src/types/NextHandler.ts @@ -1,4 +1,6 @@ -type NextHandler = () => Promise; +import Response from '../models/Response.js'; + +type NextHandler = () => Promise; export default NextHandler; diff --git a/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts b/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts index 055f2fd8..388812f4 100644 --- a/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts +++ b/packages/runtime/test/_fixtures/interfaces/Middleware.fixture.ts @@ -1,44 +1,47 @@ import Request from '../../../src/models/Request'; +import Response from '../../../src/models/Response'; import Middleware from '../../../src/interfaces/Middleware'; class FirstMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(request: Request, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { request.setHeader('first', 'yes'); request.setHeader('last', '1'); - const result = await next(); + const response = await next(); + response.result = '1' + response.result; - return '1' + result; + return response; } } class SecondMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(request: Request, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { request.setHeader('second', 'yes'); request.setHeader('last', '2'); - const result = await next(); + const response = await next(); + response.result = '2' + response.result; - return '2' + result; + return response; } } class ThirdMiddleware implements Middleware { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async handle(request: Request, next: () => Promise): Promise + async handle(request: Request, next: () => Promise): Promise { request.setHeader('third', 'yes'); request.setHeader('last', '3'); - return '3'; + return new Response('3'); } } diff --git a/packages/runtime/test/services/LocalGateway.spec.ts b/packages/runtime/test/services/LocalGateway.spec.ts index fa616e68..e054a023 100644 --- a/packages/runtime/test/services/LocalGateway.spec.ts +++ b/packages/runtime/test/services/LocalGateway.spec.ts @@ -57,17 +57,17 @@ describe('services/LocalGateway', () => it('should find and run a procedure from a node', async () => { const request = new Request('second', Version.DEFAULT, new Map(), new Map()); - const firstResult = await gateway.run(request); + const response = await gateway.run(request); - expect(firstResult).toBe('first'); + expect(response.result).toBe('first'); }); it('should find and run a procedure from a node that calls a procedure on another node', async () => { const request = new Request('third', Version.DEFAULT, new Map(), new Map()); - const result = await gateway.run(request); + const response = await gateway.run(request); - expect(result).toBe('fourth'); + expect(response.result).toBe('fourth'); }); it('should not run a non-existing procedure', async () => diff --git a/packages/runtime/test/services/LocalNode.spec.ts b/packages/runtime/test/services/LocalNode.spec.ts index 7b70cb92..0de6394d 100644 --- a/packages/runtime/test/services/LocalNode.spec.ts +++ b/packages/runtime/test/services/LocalNode.spec.ts @@ -54,25 +54,25 @@ describe('services/LocalNode', () => it('should run a public procedure that calls a private procedure on the same segment', async () => { const request = new Request('second', Version.DEFAULT, new Map(), new Map()); - const result = await node.run(request); + const response = await node.run(request); - expect(result).toBe('first'); + expect(response.result).toBe('first'); }); it('should run a public procedure that calls a private procedure on another segment', async () => { const request = new Request('sixth', Version.DEFAULT, new Map(), new Map()); - const result = await node.run(request); + const response = await node.run(request); - expect(result).toBe('first'); + expect(response.result).toBe('first'); }); it('should run a public procedure that calls a public procedure on another segment', async () => { const request = new Request('third', Version.DEFAULT, new Map(), new Map()); - const result = await node.run(request); + const response = await node.run(request); - expect(result).toBe('fourth'); + expect(response.result).toBe('fourth'); }); it('should not run a non-existing procedure', async () => diff --git a/packages/runtime/test/services/ProcedureRuntime.spec.ts b/packages/runtime/test/services/ProcedureRuntime.spec.ts index 9ab8f466..47afaf95 100644 --- a/packages/runtime/test/services/ProcedureRuntime.spec.ts +++ b/packages/runtime/test/services/ProcedureRuntime.spec.ts @@ -18,9 +18,9 @@ describe('services/ProcedureRuntime', () => const headers = new Map(); const request = new Request('test', new Version(1, 0, 0), args, headers); - const result = await runtime.handle(request); + const response = await runtime.handle(request); - expect(result).toBe('123'); + expect(response.result).toBe('123'); expect(headers.get('first')).toBe('yes'); expect(headers.get('second')).toBe('yes'); expect(headers.get('third')).toBe('yes'); diff --git a/packages/server-nodejs/src/controllers/RPCController.ts b/packages/server-nodejs/src/controllers/RPCController.ts index fcb1e752..febc2373 100644 --- a/packages/server-nodejs/src/controllers/RPCController.ts +++ b/packages/server-nodejs/src/controllers/RPCController.ts @@ -160,14 +160,14 @@ export default class RPCController const deserializedArgs = await this.#serializer.deserialize(args) as Record; const argsMap = new Map(Object.entries(deserializedArgs)); - const request = new JitarRequest(fqn, version, argsMap, headers); - const result = await this.#runtime.handle(request); + const runtimeRequest = new JitarRequest(fqn, version, argsMap, headers); + const runtimeResponse = await this.#runtime.handle(runtimeRequest); this.#logger.info(`Ran procedure -> ${fqn} (v${version.toString()})`); - this.#setResponseHeaders(response, headers); + this.#setResponseHeaders(response, runtimeResponse.headers); - return this.#createResultResponse(result, response, serialize); + return this.#createResultResponse(runtimeResponse.result, response, serialize); } catch (error: unknown) { diff --git a/packages/server-nodejs/src/middleware/CorsMiddleware.ts b/packages/server-nodejs/src/middleware/CorsMiddleware.ts index b76de7f3..6dec1bc9 100644 --- a/packages/server-nodejs/src/middleware/CorsMiddleware.ts +++ b/packages/server-nodejs/src/middleware/CorsMiddleware.ts @@ -1,5 +1,5 @@ -import { Middleware, NextHandler, Request } from '@jitar/runtime'; +import { Middleware, NextHandler, Request, Response } from '@jitar/runtime'; export default class CorsMiddleware implements Middleware { @@ -19,19 +19,19 @@ export default class CorsMiddleware implements Middleware get allowHeaders() { return this.#allowHeaders; } - async handle(request: Request, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { - const result = await next(); + const response = await next(); - this.#setHeaders(request); + this.#setHeaders(response); - return result; + return response; } - #setHeaders(request: Request): void + #setHeaders(response: Response): void { - request.setHeader('Access-Control-Allow-Origin', this.#allowOrigin); - request.setHeader('Access-Control-Allow-Methods', this.#allowMethods); - request.setHeader('Access-Control-Allow-Headers', this.#allowHeaders); + response.setHeader('Access-Control-Allow-Origin', this.#allowOrigin); + response.setHeader('Access-Control-Allow-Methods', this.#allowMethods); + response.setHeader('Access-Control-Allow-Headers', this.#allowHeaders); } } From 47b303d0577d13e035632793b9d1d05bf9104c33 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 5 Dec 2023 10:46:17 +0100 Subject: [PATCH 6/9] #237: Extended the middleware documentation with additional request and response information. --- documentation/docs/develop/middleware.md | 34 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/documentation/docs/develop/middleware.md b/documentation/docs/develop/middleware.md index f9c8337c..ff29e6cb 100644 --- a/documentation/docs/develop/middleware.md +++ b/documentation/docs/develop/middleware.md @@ -17,7 +17,6 @@ Middleware provides a way to hook into Jitars automated communication system. It In this section you'll learn how to create and add your own middleware. - ## Creating middleware Any middleware required to implement Jitars Middleware interface. This interface has a single function to handle the request. @@ -34,14 +33,43 @@ export default class MyMiddleware implements Middleware const response = await next(); - // Modify the response (result) here + // Modify the response (result and headers) here return response; } } ``` -The `fqn`, `version` and `next` parameters are immutable, so only the args and headers can be modified. The args provide the procedure arguments. The headers contain the HTTP-headers that provide meta-information like authentication. +The `request` parameter contains all request information including the arguments and headers. It has the following interface. + +```ts +/* Properties */ +const fqn = request.fqn; // readonly +const version = request.version; // readonly + +/* Arguments */ +request.setArgument('authenticator', authenticator); +const authenticator = request.getArgument('authenticator'); +request.removeArgument('authenticator'); + +/* Headers */ +request.setHeader('X-My-Header', 'value'); +const myHeader = request.getHeader('X-My-Header'); +request.removeHeader('X-My-Header'); +``` + +The `response` contains besides the actual value the response headers. It has the following interface. + +```ts +/* Properties */ +const result = response.result; +response.result = newResult; + +/* Headers */ +response.setHeader('X-My-Header', 'value'); +const myHeader = response.getHeader('X-My-Header'); +response.removeHeader('X-My-Header'); +``` Because all middleware is chained, the next parameter must always be called. This function does not take any arguments, all the arguments will be provided automatically. Note that the handle function is async so it can return a promise. From 95d62d7bea11e9705e025d041713ce53767c5972 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 5 Dec 2023 10:46:54 +0100 Subject: [PATCH 7/9] #237: Added the middleware migration information to a new migration document --- migrations/migrate-from-0.4.x-to-0.5.0.md | 86 +++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 migrations/migrate-from-0.4.x-to-0.5.0.md diff --git a/migrations/migrate-from-0.4.x-to-0.5.0.md b/migrations/migrate-from-0.4.x-to-0.5.0.md new file mode 100644 index 00000000..3caf0db4 --- /dev/null +++ b/migrations/migrate-from-0.4.x-to-0.5.0.md @@ -0,0 +1,86 @@ +# Migrate from 0.4.x to 0.5.0 + +The 0.5 version of Jitar introduces some breaking changes. All changes are described here, with instructions how to adopt them. + +## Middleware + +We've updated our middleware model to be more clean and extendable. + +Let's look at the 'old' implementation first. + +```ts +import { Middleware, Version, NextHandler } from 'jitar'; + +export default class MyMiddleware implements Middleware +{ + async handle(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + { + // Modify the request (args and headers) here + + const result = await next(); + + // Modify the response (result) here + + return result; + } +} +``` + +The `handle` function in this implementation takes a lot of arguments that we've combined in a `Request` object. +This makes it simpler to create middleware, and the `handle` function looks a lot cleaner. + +Also, manipulating the response headers wasn't very clear in this implementation because they were combined with the request headers. +Therefore we've created a `Response` object that combines the response value and the response headers. + +The new implementation looks as follows. + +```ts +import { Middleware, Request, Response, NextHandler } from 'jitar'; + +export default class MyMiddleware implements Middleware +{ + async handle(request: Request, next: NextHandler): Promise + { + // Modify the request (args and headers) here + + const response = await next(); + + // Modify the response (result and headers) here + + return response; + } +} +``` + +The `Request` has the following interface. + +```ts +/* Properties */ +const fqn = request.fqn; // readonly +const version = request.version; // readonly + +/* Arguments */ +request.setArgument('authenticator', authenticator); +const authenticator = request.getArgument('authenticator'); +request.removeArgument('authenticator'); + +/* Headers */ +request.setHeader('X-My-Header', 'value'); +const myHeader = request.getHeader('X-My-Header'); +request.removeHeader('X-My-Header'); +``` + +The `Response` has the following interface. + +```ts +/* Properties */ +const result = response.result; +response.result = newResult; + +/* Headers */ +response.setHeader('X-My-Header', 'value'); +const myHeader = response.getHeader('X-My-Header'); +response.removeHeader('X-My-Header'); +``` + +More information on Middleware can be found in the [documentation](https://docs.jitar.dev/develop/middleware.html). \ No newline at end of file From 1080b658dcd949f6fecea01e3835df1417115a86 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 5 Dec 2023 16:54:34 +0100 Subject: [PATCH 8/9] #237: Processed review feedback --- packages/runtime/src/models/Request.ts | 8 ++++---- packages/runtime/src/models/Response.ts | 6 +++--- packages/runtime/src/models/Segment.ts | 2 +- packages/runtime/src/services/ProcedureRunner.ts | 6 +----- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/models/Request.ts b/packages/runtime/src/models/Request.ts index 98acde4c..22ecbc65 100644 --- a/packages/runtime/src/models/Request.ts +++ b/packages/runtime/src/models/Request.ts @@ -34,17 +34,17 @@ export default class Request return this.#args.get(name); } - removeArgument(name: string) + removeArgument(name: string): void { this.#args.delete(name); } - clearHeaders() + clearHeaders(): void { this.#headers.clear(); } - setHeader(name: string, value: string) + setHeader(name: string, value: string): void { this.#headers.set(name, value); } @@ -54,7 +54,7 @@ export default class Request return this.#headers.get(name); } - removeHeader(name: string) + removeHeader(name: string): void { this.#headers.delete(name); } diff --git a/packages/runtime/src/models/Response.ts b/packages/runtime/src/models/Response.ts index 2d217509..21485f8a 100644 --- a/packages/runtime/src/models/Response.ts +++ b/packages/runtime/src/models/Response.ts @@ -16,12 +16,12 @@ export default class Response get headers() { return this.#headers; } - clearHeaders() + clearHeaders(): void { this.#headers.clear(); } - setHeader(name: string, value: string) + setHeader(name: string, value: string): void { this.#headers.set(name, value); } @@ -31,7 +31,7 @@ export default class Response return this.#headers.get(name); } - removeHeader(name: string) + removeHeader(name: string): void { this.#headers.delete(name); } diff --git a/packages/runtime/src/models/Segment.ts b/packages/runtime/src/models/Segment.ts index f07b9a0a..6f41884f 100644 --- a/packages/runtime/src/models/Segment.ts +++ b/packages/runtime/src/models/Segment.ts @@ -32,7 +32,7 @@ export default class Segment return this.#procedures.get(fqn); } - getPublicProcedures() + getPublicProcedures(): Procedure[] { const procedures = [...this.#procedures.values()]; diff --git a/packages/runtime/src/services/ProcedureRunner.ts b/packages/runtime/src/services/ProcedureRunner.ts index c73c02d1..b84dc779 100644 --- a/packages/runtime/src/services/ProcedureRunner.ts +++ b/packages/runtime/src/services/ProcedureRunner.ts @@ -18,10 +18,6 @@ export default class ProcedureRunner implements Middleware // eslint-disable-next-line @typescript-eslint/no-unused-vars async handle(request: Request, next: NextHandler): Promise { - const result = await this.#runner.run(request); - - request.clearHeaders(); - - return result; + return this.#runner.run(request); } } From 898e77ff348b3f1986e7fbd5b9df8e79eaa9842e Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 5 Dec 2023 22:37:12 +0100 Subject: [PATCH 9/9] #237: Added last missing return type --- packages/runtime/src/models/Request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/models/Request.ts b/packages/runtime/src/models/Request.ts index 22ecbc65..cf8d2482 100644 --- a/packages/runtime/src/models/Request.ts +++ b/packages/runtime/src/models/Request.ts @@ -24,7 +24,7 @@ export default class Request get headers() { return this.#headers; } - setArgument(name: string, value: unknown) + setArgument(name: string, value: unknown): void { this.#args.set(name, value); }