Skip to content

Commit

Permalink
🔀 Merge pull request #1324 from jovotech/v4/dev
Browse files Browse the repository at this point in the history
🔖 Prepare latest release
  • Loading branch information
Jan König authored May 17, 2022
2 parents b9f7fa3 + 92cc9c6 commit fd8c7f9
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

name: jovo-framework workflow

on: [push]
on: [push, pull_request]

jobs:
build:
Expand Down
10 changes: 7 additions & 3 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ yourHandler() {

### Redirect to Components

If you `$redirect()` to a different component, the current one is removed from the [`$state` stack](./state-stack.md). You can see this as a permanent redirect.
If you `$redirect()` to a different component, the current [`$state` stack](./state-stack.md) is cleared. You can see this as a permanent redirect. We recommend redirects if you want to move from one isolated part (or component) of a conversation to another. If you want to keep the state when moving between components, we recommend using [`$delegate()`](#delegate-to-components).

If no handler name is specified, the redirect triggers the other component's `START` handler.

Expand Down Expand Up @@ -288,13 +288,17 @@ UNHANDLED() {

By default, the current component's `UNHANDLED` gets prioritized over global handlers in other components. Learn more about [`UNHANDLED` prioritization in the routing documentation](./routing.md#unhandled-prioritization).


## Middlewares

The `event.ComponentTreeNode.executeHandler` [event middleware](./middlewares.md#event-middlewares) gets called every time a handler is executed. For example, you can [hook](./hooks.md) into it like this:

```typescript
app.hook('after.event.ComponentTreeNode.executeHandler', (jovo: Jovo): void => {
app.hook('after.event.ComponentTreeNode.executeHandler', (jovo: Jovo, payload): void => {
// ...
});
```

The `payload` includes the following properties:

- `componentName`
- `handler`
41 changes: 34 additions & 7 deletions docs/middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,45 @@ There are two types of event middlewares:
- Public methods like [`$resolve`](./handlers.md#resolve-a-component) can be accessed using `event.$resolve`
- Some "under the hood" methods can be accessed using the class name and the method name, for example `event.ComponentTreeNode.executeHandler`

These middlewares can also come with a `payload` that you can access in your hook or plugin as second parameter, for example:

```typescript
app.hook('event.$resolve', async (jovo: Jovo, payload): Promise<void> => {
const resolvedHandler = payload.resolvedHandler;
// ...
});
```

Find all current event middlewares in the table below:

| Middleware | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event.$resolve` | [`$resolve`](./handlers.md#resolve-a-component) is called in a handler |
| `event.$redirect` | [`$redirect`](./handlers.md#redirect-to-components) is called in a handler |
| `event.$delegate` | [`$delegate`](./handlers.md#delegate-to-components) is called in a handler |
| `event.$send` | [`$send`](./output.md#send-a-message) is called in a handler |
| `event.ComponentTreeNode.executeHandler` | This event is called whenever a new handler gets executed. Part of the [`ComponentTreeNode` class](https://github.com/jovotech/jovo-framework/blob/v4/latest/framework/src/ComponentTreeNode.ts). See the [`ComponentTree` section](./components.md#componenttree) for more information. |
| Middleware | Description | Payload |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `event.$resolve` | [`$resolve`](./handlers.md#resolve-a-component) is called in a handler | `resolvedHandler: string`, `eventName: string`, `eventArgs: ARGS extends unknown[]` |
| `event.$redirect` | [`$redirect`](./handlers.md#redirect-to-components) is called in a handler | `componentName: string`, `handler: string` |
| `event.$delegate` | [`$delegate`](./handlers.md#delegate-to-components) is called in a handler | `componentName: string`, `options: DelegateOptions` |
| `event.$send` | [`$send`](./output.md#send-a-message) is called in a handler | `outputConstructorOrTemplateOrMessage`, `options` |
| `event.ComponentTreeNode.executeHandler` | This event is called whenever a new handler gets executed. Part of the [`ComponentTreeNode` class](https://github.com/jovotech/jovo-framework/blob/v4/latest/framework/src/ComponentTreeNode.ts). See the [`ComponentTree` section](./components.md#componenttree) for more information. | `componentName: string`, `handler: string` |

## Middleware Features

### Custom Middlewares

You can also use the `$handleRequest` object to run your own middlewares, for example:

```typescript
await jovo.$handleRequest.middlewareCollection.run('<YOUR_MIDDLEWARE_NAME>', jovo, payload);
```

The `payload` is of the type `AnyObject`, so you can pass any object to the middleware, for example `{ name: 'SomeName' }`.

Using a [hook](./hooks.md) or a [plugin](./plugins.md), you can then hook into this middleware:

```typescript
app.hook('<YOUR_MIDDLEWARE_NAME>', async (jovo: Jovo, payload): Promise<void> => {
// ...
});
```

### Stop the Middleware Execution

Either a [hook](./hooks.md) or a [plugin](./plugins.md) can use `stopMiddlewareExecution` to remove all middlewares from the middleware collection of `HandleRequest` and its plugins. This way, all following middlewares won't be executed.
Expand Down
5 changes: 4 additions & 1 deletion framework/src/ComponentTreeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ export class ComponentTreeNode<COMPONENT extends BaseComponent = BaseComponent>
}

// Run any middlewares that are attached to 'event.ComponentTreeNode.executeHandler'
await jovo.$handleRequest.middlewareCollection.run(EXECUTE_HANDLER_MIDDLEWARE, jovo);
await jovo.$handleRequest.middlewareCollection.run(EXECUTE_HANDLER_MIDDLEWARE, jovo, {
component: this.name,
handler,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (componentInstance as any)[handler](...(callArgs || []));
Expand Down
35 changes: 23 additions & 12 deletions framework/src/Jovo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ export abstract class Jovo<
// push the new OutputTemplate(s) to $output
Array.isArray(newOutput) ? this.$output.push(...newOutput) : this.$output.push(newOutput);

await this.$handleRequest.middlewareCollection.run(SEND_MIDDLEWARE, this);
await this.$handleRequest.middlewareCollection.run(SEND_MIDDLEWARE, this, {
outputConstructorOrTemplateOrMessage,
options,
});
}

async $redirect<
Expand All @@ -314,24 +317,25 @@ export abstract class Jovo<
this.$handleRequest.activeComponentNode?.path,
);

// update the state-stack if the component is not global
// clear the state stack
this.$session.state = [];

// add new component to the stack if it's not global
// @see https://www.jovo.tech/docs/components#global-components
if (!componentNode.metadata.isGlobal) {
const stackItem: StateStackItem = {
component: componentNode.path.join('.'),
};
if (!this.$state?.length) {
// initialize the state-stack if it is empty or does not exist
this.$session.state = [stackItem];
} else {
// replace last item in stack
this.$state[this.$state.length - 1] = stackItem;
}
this.$session.state.push(stackItem);
}

// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = componentNode;

await this.$handleRequest.middlewareCollection.run(REDIRECT_MIDDLEWARE, this);
await this.$handleRequest.middlewareCollection.run(REDIRECT_MIDDLEWARE, this, {
componentName,
handler,
});

// execute the component's handler
await componentNode.executeHandler({
Expand Down Expand Up @@ -405,7 +409,10 @@ export abstract class Jovo<
// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = componentNode;

await this.$handleRequest.middlewareCollection.run(DELEGATE_MIDDLEWARE, this);
await this.$handleRequest.middlewareCollection.run(DELEGATE_MIDDLEWARE, this, {
componentName,
options,
});

// execute the component's handler
await componentNode.executeHandler({
Expand Down Expand Up @@ -440,7 +447,11 @@ export abstract class Jovo<
// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = previousComponentNode;

await this.$handleRequest.middlewareCollection.run(RESOLVE_MIDDLEWARE, this);
await this.$handleRequest.middlewareCollection.run(RESOLVE_MIDDLEWARE, this, {
resolvedHandler,
eventName,
eventArgs,
});

// execute the component's handler
await previousComponentNode.executeHandler({
Expand Down
7 changes: 4 additions & 3 deletions framework/src/Middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Jovo } from './Jovo';
import { AnyObject } from '@jovotech/common';

export type MiddlewareFunction = (jovo: Jovo) => Promise<unknown> | unknown;
export type MiddlewareFunction = (jovo: Jovo, payload?: AnyObject) => Promise<unknown> | unknown;

export class Middleware<NAME extends string = string> {
readonly fns: MiddlewareFunction[];
Expand All @@ -15,12 +16,12 @@ export class Middleware<NAME extends string = string> {
return this;
}

async run(jovo: Jovo): Promise<void> {
async run(jovo: Jovo, payload?: AnyObject): Promise<void> {
if (!this.enabled) {
return;
}
for (let i = 0, len = this.fns.length; i < len; i++) {
await this.fns[i](jovo);
await this.fns[i](jovo, payload);
}
}

Expand Down
25 changes: 17 additions & 8 deletions framework/src/MiddlewareCollection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrayElement } from '@jovotech/common';
import { AnyObject, ArrayElement } from '@jovotech/common';
import { Jovo } from './index';
import { Middleware, MiddlewareFunction } from './Middleware';

Expand Down Expand Up @@ -77,30 +77,39 @@ export class MiddlewareCollection<MIDDLEWARES extends string[] = string[]> {
return this;
}

async run(name: PossibleMiddlewareNames<MIDDLEWARES>, jovo: Jovo): Promise<void>;
async run(name: string, jovo: Jovo): Promise<void>;
async run(names: PossibleMiddlewareNames<MIDDLEWARES>[], jovo: Jovo): Promise<void>;
async run(names: string[], jovo: Jovo): Promise<void>;
async run(
name: PossibleMiddlewareNames<MIDDLEWARES>,
jovo: Jovo,
payload?: AnyObject,
): Promise<void>;
async run(name: string, jovo: Jovo, payload?: AnyObject): Promise<void>;
async run(
names: PossibleMiddlewareNames<MIDDLEWARES>[],
jovo: Jovo,
payload?: AnyObject,
): Promise<void>;
async run(names: string[], jovo: Jovo, payload?: AnyObject): Promise<void>;
async run(
nameOrNames:
| string
| PossibleMiddlewareNames<MIDDLEWARES>
| Array<string | PossibleMiddlewareNames<MIDDLEWARES>>,
jovo: Jovo,
payload?: AnyObject,
): Promise<void> {
const names = typeof nameOrNames === 'string' ? [nameOrNames] : nameOrNames;
for (const name of names) {
const beforeName = `before.${name}`;
if (this.has(beforeName)) {
await this.run(beforeName, jovo);
await this.run(beforeName, jovo, payload);
}

const middleware = this.get(name);
await middleware?.run(jovo);
await middleware?.run(jovo, payload);

const afterName = `after.${name}`;
if (this.has(afterName)) {
await this.run(afterName, jovo);
await this.run(afterName, jovo, payload);
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions framework/src/testsuite/TestSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export class TestSuite<PLATFORM extends Platform = TestPlatform> extends Plugin<
: this.requestOrInput.type === InputType.Launch
? this.requestBuilder.launch()
: this.requestBuilder.intent();

await this.app.handle(new TestServer(request));
}

Expand Down Expand Up @@ -227,7 +226,10 @@ export class TestSuite<PLATFORM extends Platform = TestPlatform> extends Plugin<
// Set session data
jovo.$session.isNew = false;

Object.assign(this, jovo);
_merge(this.$user.data, jovo.$user.data);
_merge(this.$session, jovo.$session);
_merge(this.$response, jovo.$response);
_merge(this.$output, jovo.$output);
}

private loadApp(): App {
Expand Down

0 comments on commit fd8c7f9

Please sign in to comment.