Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

237 middleware per procedure #388

Merged
merged 10 commits into from
Dec 6, 2023
42 changes: 35 additions & 7 deletions documentation/docs/develop/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>
async handle(request: Request, next: NextHandler): Promise<Response>
{
// 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.

Expand Down
8 changes: 4 additions & 4 deletions documentation/docs/develop/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>
async handle(request: Request, next: NextHandler): Promise<Response>
{
// Get Authorization header
// Authenticate the user
Expand All @@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>
async handle(request: Request, next: NextHandler): Promise<Response>
{
// Get user info from the args (remove if needed)
// Authorize the user (RBAC, ABAC, …)
Expand Down
8 changes: 4 additions & 4 deletions examples/concepts/middleware/src/LoggingMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>
async handle(request: Request, next: NextHandler): Promise<Response>
{
// 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;
}
Expand Down
86 changes: 86 additions & 0 deletions migrations/migrate-from-0.4.x-to-0.5.0.md
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>
{
// 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<Response>
{
// 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).
2 changes: 2 additions & 0 deletions packages/jitar/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export
HealthCheck,
Middleware,
NextHandler,
Request,
Response,
Segment,
Procedure,
Implementation,
Expand Down
11 changes: 7 additions & 4 deletions packages/runtime/src/hooks/runtime.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<unknown>
export async function runProcedure(fqn: string, versionNumber: string, args: object, sourceRequest?: Request): Promise<unknown>
{
if (_runtime === undefined)
{
Expand All @@ -22,7 +22,10 @@ export async function runProcedure(fqn: string, versionNumber: string, args: obj

const version = VersionParser.parse(versionNumber);
const argsMap = new Map<string, unknown>(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;
}
5 changes: 3 additions & 2 deletions packages/runtime/src/interfaces/Middleware.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, headers: Map<string, string>, next: NextHandler): Promise<unknown>;
handle(request: Request, next: NextHandler): Promise<Response>;
}

export default Middleware;
5 changes: 3 additions & 2 deletions packages/runtime/src/interfaces/Runner.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, headers: Map<string, string>): Promise<unknown>;
run(request: Request): Promise<Response>;
}

export default Runner;
2 changes: 2 additions & 0 deletions packages/runtime/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
12 changes: 0 additions & 12 deletions packages/runtime/src/models/Context.ts

This file was deleted.

61 changes: 61 additions & 0 deletions packages/runtime/src/models/Request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

import Version from './Version.js';

export default class Request
{
#fqn: string;
#version: Version;
#args: Map<string, unknown>;
#headers: Map<string, string> = new Map();

constructor(fqn: string, version: Version, args: Map<string, unknown>, headers: Map<string, string>)
{
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);
}
}
38 changes: 38 additions & 0 deletions packages/runtime/src/models/Response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

export default class Response
{
#result: unknown;
#headers: Map<string, string>;

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