diff --git a/documentation/docs/develop/middleware.md b/documentation/docs/develop/middleware.md index 144b270e..ff29e6cb 100644 --- a/documentation/docs/develop/middleware.md +++ b/documentation/docs/develop/middleware.md @@ -17,31 +17,59 @@ 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. ```ts // src/MyMiddleware.ts -import { Middleware, Version, NextHandler } from 'jitar'; +import { Middleware, Request, Response, 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 - const result = await next(); + const response = await next(); - // Modify the response (result) here + // Modify the response (result and headers) here - return result; + 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. diff --git a/documentation/docs/develop/security.md b/documentation/docs/develop/security.md index 8d8aab6a..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, Version, NextHandler } from 'jitar'; +import { Middleware, Request, Response, 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, Response, 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, …) diff --git a/examples/concepts/middleware/src/LoggingMiddleware.ts b/examples/concepts/middleware/src/LoggingMiddleware.ts index 52e0b051..64f66b6a 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, Response, 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/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 diff --git a/packages/jitar/src/client.ts b/packages/jitar/src/client.ts index e51e69f0..da3c725b 100644 --- a/packages/jitar/src/client.ts +++ b/packages/jitar/src/client.ts @@ -4,6 +4,8 @@ export HealthCheck, Middleware, NextHandler, + Request, + Response, Segment, Procedure, Implementation, diff --git a/packages/runtime/src/hooks/runtime.ts b/packages/runtime/src/hooks/runtime.ts index 1e26ecd5..9e63251c 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,10 @@ 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); + const targetResponse = await _runtime.run(targetRequest); + + return targetResponse.result; } diff --git a/packages/runtime/src/interfaces/Middleware.ts b/packages/runtime/src/interfaces/Middleware.ts index 82aca218..ea5c0b3d 100644 --- a/packages/runtime/src/interfaces/Middleware.ts +++ b/packages/runtime/src/interfaces/Middleware.ts @@ -1,11 +1,12 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.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..8387c886 100644 --- a/packages/runtime/src/interfaces/Runner.ts +++ b/packages/runtime/src/interfaces/Runner.ts @@ -1,9 +1,10 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.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..a3c16eb5 100644 --- a/packages/runtime/src/lib.ts +++ b/packages/runtime/src/lib.ts @@ -44,6 +44,8 @@ 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 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/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..cf8d2482 --- /dev/null +++ b/packages/runtime/src/models/Request.ts @@ -0,0 +1,61 @@ + +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; } + + setArgument(name: string, value: unknown): void + { + this.#args.set(name, value); + } + + getArgument(name: string): unknown + { + return this.#args.get(name); + } + + removeArgument(name: string): void + { + this.#args.delete(name); + } + + clearHeaders(): void + { + this.#headers.clear(); + } + + setHeader(name: string, value: string): void + { + this.#headers.set(name, value); + } + + getHeader(name: string): string | undefined + { + return this.#headers.get(name); + } + + removeHeader(name: string): void + { + this.#headers.delete(name); + } +} diff --git a/packages/runtime/src/models/Response.ts b/packages/runtime/src/models/Response.ts new file mode 100644 index 00000000..21485f8a --- /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(): void + { + this.#headers.clear(); + } + + setHeader(name: string, value: string): void + { + this.#headers.set(name, value); + } + + getHeader(name: string): string | undefined + { + return this.#headers.get(name); + } + + 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/LocalGateway.ts b/packages/runtime/src/services/LocalGateway.ts index c0494182..7e8b8355 100644 --- a/packages/runtime/src/services/LocalGateway.ts +++ b/packages/runtime/src/services/LocalGateway.ts @@ -1,7 +1,9 @@ import ProcedureNotFound from '../errors/ProcedureNotFound.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; + import ModuleLoader from '../utils/ModuleLoader.js'; import Gateway from './Gateway.js'; @@ -92,15 +94,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..e0c460b9 100644 --- a/packages/runtime/src/services/LocalNode.ts +++ b/packages/runtime/src/services/LocalNode.ts @@ -5,10 +5,10 @@ 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 Response from '../models/Response.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 +126,38 @@ 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 + async #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); + 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 e5a002b5..b26131d0 100644 --- a/packages/runtime/src/services/NodeBalancer.ts +++ b/packages/runtime/src/services/NodeBalancer.ts @@ -1,7 +1,8 @@ import NoNodeAvailable from '../errors/NoNodeAvailable.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Node from './Node.js'; @@ -47,15 +48,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..b84dc779 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 Version from '../models/Version.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,12 +16,8 @@ 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); - - headers.clear(); - - return result; + return this.#runner.run(request); } } diff --git a/packages/runtime/src/services/ProcedureRuntime.ts b/packages/runtime/src/services/ProcedureRuntime.ts index dc0b7b93..73032c36 100644 --- a/packages/runtime/src/services/ProcedureRuntime.ts +++ b/packages/runtime/src/services/ProcedureRuntime.ts @@ -1,7 +1,10 @@ import Middleware from '../interfaces/Middleware.js'; import Runner from '../interfaces/Runner.js'; -import Version from '../models/Version.js'; + +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; + import NextHandler from '../types/NextHandler.js'; import Runtime from './Runtime.js'; @@ -22,7 +25,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,25 +42,24 @@ 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]; if (next === undefined) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return async () => {}; + return async () => new Response(); } - 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..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 Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Repository from './Repository.js'; import ProcedureRuntime from './ProcedureRuntime.js'; @@ -49,8 +50,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..91d62545 100644 --- a/packages/runtime/src/services/Remote.ts +++ b/packages/runtime/src/services/Remote.ts @@ -2,8 +2,11 @@ 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 { 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,15 +101,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 = { @@ -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 df5f0aaf..2ffd1f3c 100644 --- a/packages/runtime/src/services/RemoteGateway.ts +++ b/packages/runtime/src/services/RemoteGateway.ts @@ -1,7 +1,8 @@ import NotImplemented from '../errors/generic/NotImplemented.js'; -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Gateway from './Gateway.js'; import Node from './Node.js'; @@ -34,8 +35,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..17b210fc 100644 --- a/packages/runtime/src/services/RemoteNode.ts +++ b/packages/runtime/src/services/RemoteNode.ts @@ -1,5 +1,6 @@ -import Version from '../models/Version.js'; +import Request from '../models/Request.js'; +import Response from '../models/Response.js'; import Node from './Node.js'; import Remote from './Remote.js'; @@ -43,8 +44,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/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 4ef0c1ee..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 Version from '../../../src/models/Version'; +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(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(); + 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(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(); + 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(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'; + return new Response('3'); } } diff --git a/packages/runtime/test/services/LocalGateway.spec.ts b/packages/runtime/test/services/LocalGateway.spec.ts index e4fa36fc..e054a023 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 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 result = await gateway.run('third', Version.DEFAULT, new Map(), new Map()); + const request = new Request('third', Version.DEFAULT, new Map(), new Map()); + const response = await gateway.run(request); - expect(result).toBe('fourth'); + expect(response.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..0de6394d 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 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 result = await node.run('sixth', Version.DEFAULT, new Map(), new Map()); + const request = new Request('sixth', Version.DEFAULT, new Map(), new Map()); + 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 result = await node.run('third', Version.DEFAULT, new Map(), new Map()); + const request = new Request('third', Version.DEFAULT, new Map(), new Map()); + const response = await node.run(request); - expect(result).toBe('fourth'); + expect(response.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..47afaf95 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,9 +17,10 @@ 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 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 9c0e9b35..febc2373 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,13 +160,14 @@ 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 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) { @@ -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..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, Version } 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(fqn: string, version: Version, args: Map, headers: Map, next: NextHandler): Promise + async handle(request: Request, next: NextHandler): Promise { - const result = await next(); + const response = await next(); - this.#setHeaders(headers); + this.#setHeaders(response); - return result; + return response; } - #setHeaders(headers: Map): void + #setHeaders(response: Response): 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); + response.setHeader('Access-Control-Allow-Origin', this.#allowOrigin); + response.setHeader('Access-Control-Allow-Methods', this.#allowMethods); + response.setHeader('Access-Control-Allow-Headers', this.#allowHeaders); } }