From 82c72d3f756ec4e5b2ddc86957b70660c7fe22ce Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 2 Jun 2022 08:43:43 -0500 Subject: [PATCH] feat(event-bridge): schedule, whenAny, rename, when without scope, docs (#161) BREAKING_CHANGE: Event bus integration from `Function` and `StepFunction` has been updated to use the pattern `bus.putEvent(event)`. Previously events were sent by invoking the bus `bus(event)`. BREAKING_CHANGE: Type names have been changed: `EventtBusRule` ->`Rule`, `EventBusTransform` -> `EventTransform`, `EventBusRuleInput` -> `Event` --- src/checker.ts | 56 +-- src/compile.ts | 46 +- src/event-bridge/event-bus.ts | 458 ++++++++++++------ src/event-bridge/event-pattern/pattern.ts | 8 +- src/event-bridge/rule.ts | 124 +++-- src/event-bridge/target-input.ts | 2 +- src/event-bridge/target.ts | 23 +- src/event-bridge/transform.ts | 31 +- src/event-bridge/types.ts | 2 +- src/integration.ts | 8 +- src/step-function.ts | 38 +- test-app/src/app.ts | 8 +- test-app/src/func-test.ts | 2 +- test-app/src/message-board.ts | 21 +- test-app/src/people-events.ts | 2 +- test/eventbus.localstack.test.ts | 12 +- test/eventbus.test.ts | 177 ++++++- test/eventpattern.test.ts | 270 +++++------ test/eventtargets.test.ts | 8 +- test/function.localstack.test.ts | 14 +- test/step-function.test.ts | 18 +- test/util.ts | 6 +- .../docs/concepts/event-bridge/event-bus.md | 332 +------------ .../concepts/event-bridge/event-sources.md | 34 ++ website/docs/concepts/event-bridge/index.md | 259 +++++++++- .../concepts/event-bridge/integrations.md | 120 +++++ .../docs/concepts/event-bridge/limitations.md | 98 ++++ website/docs/concepts/event-bridge/rule.md | 124 +++++ website/docs/concepts/event-bridge/syntax.md | 50 +- .../docs/concepts/event-bridge/transform.md | 106 ++++ .../concepts/step-function/event-sources.md | 52 ++ 31 files changed, 1666 insertions(+), 843 deletions(-) create mode 100644 website/docs/concepts/event-bridge/event-sources.md create mode 100644 website/docs/concepts/event-bridge/integrations.md create mode 100644 website/docs/concepts/event-bridge/limitations.md create mode 100644 website/docs/concepts/event-bridge/transform.md create mode 100644 website/docs/concepts/step-function/event-sources.md diff --git a/src/checker.ts b/src/checker.ts index d11f1218..2820eb81 100644 --- a/src/checker.ts +++ b/src/checker.ts @@ -1,8 +1,8 @@ import * as ts from "typescript"; import * as tsserver from "typescript/lib/tsserverlibrary"; import { AppsyncResolver } from "./appsync"; -import { EventBus, EventBusRule } from "./event-bridge"; -import { EventBusTransform } from "./event-bridge/transform"; +import { EventBus, Rule } from "./event-bridge"; +import { EventTransform } from "./event-bridge/transform"; import { Function } from "./function"; import { ExpressStepFunction, StepFunction } from "./step-function"; @@ -17,11 +17,11 @@ export type TsFunctionParameter = | ts.ElementAccessExpression | ts.CallExpression; -export type EventBusRuleInterface = ts.NewExpression & { +export type RuleInterface = ts.NewExpression & { arguments: [any, any, any, TsFunctionParameter]; }; -export type EventBusTransformInterface = ts.NewExpression & { +export type EventTransformInterface = ts.NewExpression & { arguments: [TsFunctionParameter, any]; }; @@ -64,11 +64,11 @@ export function makeFunctionlessChecker( ...checker, isConstant, isAppsyncResolver, - isEventBusRuleMapFunction, + isRuleMapFunction, isEventBusWhenFunction, isFunctionlessType, - isNewEventBusRule, - isNewEventBusTransform, + isNewRule, + isNewEventTransform, isReflectFunction, isStepFunction, isNewFunctionlessFunction, @@ -78,26 +78,24 @@ export function makeFunctionlessChecker( /** * Matches the patterns: - * * new EventBusRule() + * * new Rule() */ - function isNewEventBusRule(node: ts.Node): node is EventBusRuleInterface { - return ts.isNewExpression(node) && isEventBusRule(node.expression); + function isNewRule(node: ts.Node): node is RuleInterface { + return ts.isNewExpression(node) && isRule(node.expression); } /** * Matches the patterns: - * * new EventBusTransform() + * * new EventTransform() */ - function isNewEventBusTransform( - node: ts.Node - ): node is EventBusTransformInterface { - return ts.isNewExpression(node) && isEventBusTransform(node.expression); + function isNewEventTransform(node: ts.Node): node is EventTransformInterface { + return ts.isNewExpression(node) && isEventTransform(node.expression); } /** * Matches the patterns: * * IEventBus.when - * * IEventBusRule.when + * * IRule.when */ function isEventBusWhenFunction( node: ts.Node @@ -107,22 +105,20 @@ export function makeFunctionlessChecker( ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "when" && (isEventBus(node.expression.expression) || - isEventBusRule(node.expression.expression)) + isRule(node.expression.expression)) ); } /** * Matches the patterns: - * * IEventBusRule.map() + * * IRule.map() */ - function isEventBusRuleMapFunction( - node: ts.Node - ): node is EventBusMapInterface { + function isRuleMapFunction(node: ts.Node): node is EventBusMapInterface { return ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && - isEventBusRule(node.expression.expression) + isRule(node.expression.expression) ); } @@ -138,25 +134,25 @@ export function makeFunctionlessChecker( } /** - * Checks to see if a node is of type {@link EventBusRule}. + * Checks to see if a node is of type {@link Rule}. * The node could be any kind of node that returns an event bus rule. * * Matches the patterns: - * * IEventBusRule + * * IRule */ - function isEventBusRule(node: ts.Node) { - return isFunctionlessClassOfKind(node, EventBusRule.FunctionlessType); + function isRule(node: ts.Node) { + return isFunctionlessClassOfKind(node, Rule.FunctionlessType); } /** - * Checks to see if a node is of type {@link EventBusTransform}. + * Checks to see if a node is of type {@link EventTransform}. * The node could be any kind of node that returns an event bus rule. * * Matches the patterns: - * * IEventBusTransform + * * IEventTransform */ - function isEventBusTransform(node: ts.Node) { - return isFunctionlessClassOfKind(node, EventBusTransform.FunctionlessType); + function isEventTransform(node: ts.Node) { + return isFunctionlessClassOfKind(node, EventTransform.FunctionlessType); } function isAppsyncResolver(node: ts.Node): node is ts.NewExpression & { diff --git a/src/compile.ts b/src/compile.ts index 71c4fed5..139d0429 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -5,8 +5,8 @@ import ts from "typescript"; import { assertDefined } from "./assert"; import { EventBusMapInterface, - EventBusRuleInterface, - EventBusTransformInterface, + RuleInterface, + EventTransformInterface, EventBusWhenInterface, FunctionInterface, makeFunctionlessChecker, @@ -106,11 +106,11 @@ export function compile( ); } else if (checker.isEventBusWhenFunction(node)) { return visitEventBusWhen(node); - } else if (checker.isEventBusRuleMapFunction(node)) { + } else if (checker.isRuleMapFunction(node)) { return visitEventBusMap(node); - } else if (checker.isNewEventBusRule(node)) { - return visitEventBusRule(node); - } else if (checker.isNewEventBusTransform(node)) { + } else if (checker.isNewRule(node)) { + return visitRule(node); + } else if (checker.isNewEventTransform(node)) { return visitEventTransform(node); } else if (checker.isNewFunctionlessFunction(node)) { return visitFunction(node, ctx); @@ -155,7 +155,7 @@ export function compile( ); } - function visitEventBusRule(call: EventBusRuleInterface): ts.Node { + function visitRule(call: RuleInterface): ts.Node { const [one, two, three, impl] = call.arguments; return ts.factory.updateNewExpression( @@ -171,7 +171,7 @@ export function compile( ); } - function visitEventTransform(call: EventBusTransformInterface): ts.Node { + function visitEventTransform(call: EventTransformInterface): ts.Node { const [impl, ...rest] = call.arguments; return ts.factory.updateNewExpression( @@ -183,14 +183,26 @@ export function compile( } function visitEventBusWhen(call: EventBusWhenInterface): ts.Node { - const [one, two, impl] = call.arguments; + // support the 2 or 3 parameter when. + if (call.arguments.length === 3) { + const [one, two, impl] = call.arguments; - return ts.factory.updateCallExpression( - call, - call.expression, - call.typeArguments, - [one, two, errorBoundary(() => toFunction("FunctionDecl", impl))] - ); + return ts.factory.updateCallExpression( + call, + call.expression, + call.typeArguments, + [one, two, errorBoundary(() => toFunction("FunctionDecl", impl))] + ); + } else { + const [one, impl] = call.arguments; + + return ts.factory.updateCallExpression( + call, + call.expression, + call.typeArguments, + [one, errorBoundary(() => toFunction("FunctionDecl", impl))] + ); + } } function visitEventBusMap(call: EventBusMapInterface): ts.Node { @@ -288,7 +300,7 @@ export function compile( * const bus = new EventBus() // an example of an Integration, could be any Integration * * (arg1: string) => { - * bus({ source: "src" }) + * bus.putEvents({ source: "src" }) * } * ``` * @@ -1039,7 +1051,7 @@ export function compile( * const bus = new EventBus() * new Function(() => { * const busbus = bus; - * busbus(...) + * busbus.putEvents(...) * }) * ``` * diff --git a/src/event-bridge/event-bus.ts b/src/event-bridge/event-bus.ts index 95898bec..2e51d7cd 100644 --- a/src/event-bridge/event-bus.ts +++ b/src/event-bridge/event-bus.ts @@ -14,26 +14,25 @@ import { PropAssignExpr, StringLiteralExpr, } from "../expression"; +import { Function, NativePreWarmContext, PrewarmClients } from "../function"; +import { Integration, IntegrationCall, makeIntegration } from "../integration"; import { - Function, - NativeIntegration, - NativePreWarmContext, - PrewarmClients, -} from "../function"; -import { Integration } from "../integration"; -import { EventBusRule, EventPredicateFunction } from "./rule"; -import { EventBusRuleInput } from "./types"; + Rule, + RulePredicateFunction, + ImportedRule, + ScheduledEvent, + PredicateRuleBase as PredicateRuleBase, +} from "./rule"; +import { Event } from "./types"; -export const isEventBus = ( - v: any -): v is IEventBus => { +export const isEventBus = (v: any): v is IEventBus => { return ( "functionlessKind" in v && v.functionlessKind === EventBusBase.FunctionlessType ); }; -export interface IEventBusFilterable { +export interface IEventBusFilterable { /** * EventBus Rules can filter events using Functionless predicate functions. * @@ -79,6 +78,12 @@ export interface IEventBusFilterable { * when(this, 'rule', (event) => event.detail.list.includes("someValue")) * ``` * + * Omitting the scope will use the bus as the scope. + * + * ```ts + * when('rule', ...) + * ``` + * * Unsupported by Event Bridge * * OR Logic between multiple fields * * AND logic between most logic on a single field (except for numeric ranges.) @@ -91,14 +96,18 @@ export interface IEventBusFilterable { * Unsupported by Functionless: * * Variables from outside of the function scope */ + when( + id: string, + predicate: RulePredicateFunction + ): Rule; when( scope: Construct, id: string, - predicate: EventPredicateFunction - ): EventBusRule; + predicate: RulePredicateFunction + ): Rule; } -export interface IEventBus +export interface IEventBus extends IEventBusFilterable { readonly bus: aws_events.IEventBus; readonly eventBusArn: string; @@ -112,12 +121,36 @@ export interface IEventBus /** * Put one or more events on an Event Bus. */ - ( + putEvents( event: EventBusPutEventInput, ...events: EventBusPutEventInput[] ): void; + + /** + * Creates a rule that matches all events on the bus. + * + * When no scope or id are given, the bus is used as the scope and the id will be `all`. + * The rule created will be a singleton. + * + * When scope and id are given, a new rule will be created each time. + * + * Like all functionless, a rule is only created when the rule is used with `.pipe` or the rule is retrieved using `.rule`. + * + * ```ts + * const bus = new EventBus(scope, 'bus'); + * const func = new Function(scope, 'func', async (payload: {id: string}) => console.log(payload.id)); + * + * bus + * .all() + * .map(event => ({id: event.id})) + * .pipe(func); + * ``` + */ + all(): PredicateRuleBase; + all(scope: Construct, id: string): PredicateRuleBase; } -abstract class EventBusBase + +abstract class EventBusBase implements IEventBus, Integration { /** @@ -129,7 +162,14 @@ abstract class EventBusBase readonly eventBusName: string; readonly eventBusArn: string; - readonly native: NativeIntegration>; + protected static singletonDefaultNode = "__DefaultBus"; + + private allRule: PredicateRuleBase | undefined; + + public readonly putEvents: IntegrationCall< + IEventBus["putEvents"], + "EventBus.putEvents" + >; constructor(readonly bus: aws_events.IEventBus) { this.eventBusName = bus.eventBusName; @@ -137,146 +177,188 @@ abstract class EventBusBase // Closure event bus base const eventBusName = this.eventBusName; - this.native = >>{ - bind: (context: Function) => { - this.bus.grantPutEventsTo(context.resource); - }, - preWarm: (prewarmContext: NativePreWarmContext) => { - prewarmContext.getOrInit(PrewarmClients.EVENT_BRIDGE); - }, - call: async (args, preWarmContext) => { - const eventBridge = preWarmContext.getOrInit( - PrewarmClients.EVENT_BRIDGE - ); - await eventBridge - .putEvents({ - Entries: args.map((event) => ({ - Detail: JSON.stringify(event.detail), - EventBusName: eventBusName, - DetailType: event["detail-type"], - Resources: event.resources, - Source: event.source, - Time: - typeof event.time === "number" - ? new Date(event.time) - : undefined, - })), - }) - .promise(); - }, - }; - } - asl(call: CallExpr, context: ASL) { - this.bus.grantPutEventsTo(context.role); + this.putEvents = makeIntegration< + IEventBus["putEvents"], + "EventBus.putEvents" + >({ + kind: "EventBus.putEvents", + asl: (call: CallExpr, context: ASL) => { + this.bus.grantPutEventsTo(context.role); - // Validate that the events are object literals. - // Then normalize nested arrays of events into a single list of events. - // TODO Relax these restrictions: https://github.com/functionless/functionless/issues/101 - const eventObjs = call.args.reduce((events: ObjectLiteralExpr[], arg) => { - if (isArrayLiteralExpr(arg.expr)) { - if (!arg.expr.items.every(isObjectLiteralExpr)) { - throw Error( - "Event Bus put events must use inline object parameters. Variable references are not supported currently." - ); - } - return [...events, ...arg.expr.items]; - } else if (isObjectLiteralExpr(arg.expr)) { - return [...events, arg.expr]; - } - throw Error( - "Event Bus put events must use inline object parameters. Variable references are not supported currently." - ); - }, []); + // Validate that the events are object literals. + // Then normalize nested arrays of events into a single list of events. + // TODO Relax these restrictions: https://github.com/functionless/functionless/issues/101 + const eventObjs = call.args.reduce( + (events: ObjectLiteralExpr[], arg) => { + if (isArrayLiteralExpr(arg.expr)) { + if (!arg.expr.items.every(isObjectLiteralExpr)) { + throw Error( + "Event Bus put events must use inline object parameters. Variable references are not supported currently." + ); + } + return [...events, ...arg.expr.items]; + } else if (isObjectLiteralExpr(arg.expr)) { + return [...events, arg.expr]; + } + throw Error( + "Event Bus put events must use inline object parameters. Variable references are not supported currently." + ); + }, + [] + ); - // The interface should prevent this. - if (eventObjs.length === 0) { - throw Error("Must provide at least one event."); - } + // The interface should prevent this. + if (eventObjs.length === 0) { + throw Error("Must provide at least one event."); + } - const propertyMap: Record = { - "detail-type": "DetailType", - account: "Account", - detail: "Detail", - id: "Id", - region: "Region", - resources: "Resources", - source: "Source", - time: "Time", - version: "Version", - }; + const propertyMap: Record = { + "detail-type": "DetailType", + account: "Account", + detail: "Detail", + id: "Id", + region: "Region", + resources: "Resources", + source: "Source", + time: "Time", + version: "Version", + }; - const events = eventObjs.map((event) => { - const props = event.properties.filter( - ( - e - ): e is PropAssignExpr & { - name: StringLiteralExpr | Identifier; - } => !(isSpreadAssignExpr(e) || isComputedPropertyNameExpr(e.name)) - ); - if (props.length < event.properties.length) { - throw Error( - "Event Bus put events must use inline objects instantiated without computed or spread keys." - ); - } - return ( - props - .map( - (prop) => - [ - isIdentifier(prop.name) ? prop.name.name : prop.name.value, - prop.expr, - ] as const - ) - .filter( - (x): x is [keyof typeof propertyMap, Expr] => - x[0] in propertyMap && !!x[1] - ) - /** - * Build the parameter payload for an event entry. - * All members must be in Pascal case. - */ - .reduce( - (acc: Record, [name, expr]) => ({ - ...acc, - [propertyMap[name]]: ASL.toJson(expr), - }), - { EventBusName: this.bus.eventBusArn } - ) - ); - }); + const events = eventObjs.map((event) => { + const props = event.properties.filter( + ( + e + ): e is PropAssignExpr & { + name: StringLiteralExpr | Identifier; + } => !(isSpreadAssignExpr(e) || isComputedPropertyNameExpr(e.name)) + ); + if (props.length < event.properties.length) { + throw Error( + "Event Bus put events must use inline objects instantiated without computed or spread keys." + ); + } + return ( + props + .map( + (prop) => + [ + isIdentifier(prop.name) ? prop.name.name : prop.name.value, + prop.expr, + ] as const + ) + .filter( + (x): x is [keyof typeof propertyMap, Expr] => + x[0] in propertyMap && !!x[1] + ) + /** + * Build the parameter payload for an event entry. + * All members must be in Pascal case. + */ + .reduce( + (acc: Record, [name, expr]) => ({ + ...acc, + [propertyMap[name]]: ASL.toJson(expr), + }), + { EventBusName: this.bus.eventBusArn } + ) + ); + }); - return { - Resource: "arn:aws:states:::events:putEvents", - Type: "Task" as const, - Parameters: { - Entries: events, + return { + Resource: "arn:aws:states:::events:putEvents", + Type: "Task" as const, + Parameters: { + Entries: events, + }, + }; + }, + native: { + bind: (context: Function) => { + this.bus.grantPutEventsTo(context.resource); + }, + preWarm: (prewarmContext: NativePreWarmContext) => { + prewarmContext.getOrInit(PrewarmClients.EVENT_BRIDGE); + }, + call: async (args, preWarmContext) => { + const eventBridge = preWarmContext.getOrInit( + PrewarmClients.EVENT_BRIDGE + ); + await eventBridge + .putEvents({ + Entries: args.map((event) => ({ + Detail: JSON.stringify(event.detail), + EventBusName: eventBusName, + DetailType: event["detail-type"], + Resources: event.resources, + Source: event.source, + Time: + typeof event.time === "number" + ? new Date(event.time) + : undefined, + })), + }) + .promise(); + }, }, - }; + }); } - /** - * @inheritdoc - */ + when( + id: string, + predicate: RulePredicateFunction + ): Rule; when( scope: Construct, id: string, - predicate: EventPredicateFunction - ): EventBusRule { - return new EventBusRule(scope, id, this as any, predicate); + predicate: RulePredicateFunction + ): Rule; + when( + scope: Construct | string, + id?: string | RulePredicateFunction, + predicate?: RulePredicateFunction + ): Rule { + if (predicate) { + return new Rule( + scope as Construct, + id as string, + this as any, + predicate + ); + } else { + return new Rule( + this.bus, + scope as string, + this as any, + id as RulePredicateFunction + ); + } + } + + all(): PredicateRuleBase; + all(scope: Construct, id: string): PredicateRuleBase; + all(scope?: Construct, id?: string): PredicateRuleBase { + if (!scope || !id) { + if (!this.allRule) { + this.allRule = new PredicateRuleBase( + this.bus, + "all", + this as IEventBus, + // an empty doc will be converted to `{ source: [{ prefix: "" }]}` + { doc: {} } + ); + } + return this.allRule; + } + return new PredicateRuleBase(scope, id, this as IEventBus, { + doc: {}, + }); } } -export type EventBusPutEventInput = Partial & +export type EventBusPutEventInput = Partial & Pick; -interface EventBusBase { - ( - event: EventBusPutEventInput, - ...events: EventBusPutEventInput[] - ): void; -} - /** * A Functionless wrapper for a AWS CDK {@link aws_events.EventBus}. * @@ -293,7 +375,7 @@ interface EventBusBase { * } * * // An event with the payload - * interface myEvent extends EventBusRuleInput {} + * interface myEvent extends Event {} * * // A function that expects the payload. * const myLambdaFunction = new functionless.Function(this, 'myFunction', ...); @@ -324,7 +406,7 @@ interface EventBusBase { * .pipe(anotherEventBus); * ``` */ -export class EventBus extends EventBusBase { +export class EventBus extends EventBusBase { constructor(scope: Construct, id: string, props?: aws_events.EventBusProps) { super(new aws_events.EventBus(scope, id, props)); } @@ -332,14 +414,10 @@ export class EventBus extends EventBusBase { /** * Import an {@link aws_events.IEventBus} wrapped with Functionless abilities. */ - static fromBus( - bus: aws_events.IEventBus - ): IEventBus { + static fromBus(bus: aws_events.IEventBus): IEventBus { return new ImportedEventBus(bus); } - static #singletonDefaultNode = "__DefaultBus"; - /** * Retrieves the default event bus as a singleton on the given stack or the stack of the given construct. * @@ -349,27 +427,95 @@ export class EventBus extends EventBusBase { * new functionless.EventBus.fromBus(awsBus); * ``` */ - static default(stack: Stack): IEventBus; - static default(scope: Construct): IEventBus; - static default( + static default(stack: Stack): DefaultEventBus; + static default(scope: Construct): DefaultEventBus; + static default( scope: Construct | Stack - ): IEventBus { + ): DefaultEventBus { + return new DefaultEventBus(scope); + } + + /** + * Creates a schedule based event bus rule on the default bus. + * + * Always sends the {@link ScheduledEvent} event. + * + * ```ts + * // every hour + * const everyHour = EventBus.schedule(scope, 'cron', aws_events.Schedule.rate(Duration.hours(1))); + * + * const func = new Function(scope, 'func', async (payload: {id: string}) => console.log(payload.id)); + * + * everyHour + * .map(event => ({id: event.id})) + * .pipe(func); + * ``` + */ + static schedule( + scope: Construct, + id: string, + schedule: aws_events.Schedule, + props?: Omit< + aws_events.RuleProps, + "schedule" | "eventBus" | "eventPattern" | "targets" + > + ): ImportedRule { + return EventBus.default(scope).schedule(scope, id, schedule, props); + } +} + +export class DefaultEventBus extends EventBusBase { + constructor(scope: Construct) { const stack = scope instanceof Stack ? scope : Stack.of(scope); const bus = (stack.node.tryFindChild( - EventBus.#singletonDefaultNode + EventBusBase.singletonDefaultNode ) as aws_events.IEventBus) ?? aws_events.EventBus.fromEventBusName( stack, - EventBus.#singletonDefaultNode, + EventBusBase.singletonDefaultNode, "default" ); - return EventBus.fromBus(bus); + super(bus); + } + + /** + * Creates a schedule based event bus rule on the default bus. + * + * Always sends the {@link ScheduledEvent} event. + * + * ```ts + * const bus = EventBus.default(scope); + * // every hour + * const everyHour = bus.schedule(scope, 'cron', aws_events.Schedule.rate(Duration.hours(1))); + * + * const func = new Function(scope, 'func', async (payload: {id: string}) => console.log(payload.id)); + * + * everyHour + * .map(event => ({id: event.id})) + * .pipe(func); + * ``` + */ + schedule( + scope: Construct, + id: string, + schedule: aws_events.Schedule, + props?: Omit< + aws_events.RuleProps, + "schedule" | "eventBus" | "eventPattern" | "targets" + > + ): ImportedRule { + return new ImportedRule( + new aws_events.Rule(scope, id, { + ...props, + schedule, + }) + ); } } -class ImportedEventBus extends EventBusBase { +class ImportedEventBus extends EventBusBase { constructor(bus: aws_events.IEventBus) { super(bus); } diff --git a/src/event-bridge/event-pattern/pattern.ts b/src/event-bridge/event-pattern/pattern.ts index 2bc698ac..a4395275 100644 --- a/src/event-bridge/event-pattern/pattern.ts +++ b/src/event-bridge/event-pattern/pattern.ts @@ -2,7 +2,7 @@ import { assertNever } from "../../assert"; import * as functionless_event_bridge from "../types"; /** - * These are simlified and better structured interfaces/types to make it easier to work with Event Bridge Patterns. + * These are simplified and better structured interfaces/types to make it easier to work with Event Bridge Patterns. * Use the {@link patternToEventBridgePattern} to generate a valid object for event bridge. * * All patterns are applied to a single field {@link PatternDocument}s provide AND logic on multiple fields. @@ -99,7 +99,7 @@ export interface NumericRangeLimit { } /** - * A range of values from a posible {@link Number.NEGATIVE_INFINITY} to {@link Number.POSITIVE_INFINITY}. + * A range of values from a possible {@link Number.NEGATIVE_INFINITY} to {@link Number.POSITIVE_INFINITY}. * * Use a Upper and Lower bound of a single value to represent a single value. * Exclusive on upper and lower represents a NOT on the value. @@ -201,12 +201,12 @@ export const isNegativeSingleValueRange = (pattern: NumericRangePattern) => !pattern.upper.inclusive; /** - * Transforms the proprietary {@link PatternDocuemnt} into AWS's EventPattern schema. + * Transforms the proprietary {@link PatternDocument} into AWS's EventPattern schema. * https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html * * For each field, * if the field is a nested PatternDocument, recurse. - * if the field is a Pattern, output the pattern as a EvnetPattern. + * if the field is a Pattern, output the pattern as a EventPattern. * * We will not maintain empty patterns or pattern documents. */ diff --git a/src/event-bridge/rule.ts b/src/event-bridge/rule.ts index fbb34dc9..f2247d05 100644 --- a/src/event-bridge/rule.ts +++ b/src/event-bridge/rule.ts @@ -1,8 +1,8 @@ import { aws_events } from "aws-cdk-lib"; import { Construct } from "constructs"; -import { Function } from "../function"; +import { IFunction } from "../function"; import { ExpressStepFunction, StepFunction } from "../step-function"; -import { EventBus, IEventBus, IEventBusFilterable } from "./event-bus"; +import { IEventBus, IEventBusFilterable } from "./event-bus"; import { andDocuments, synthesizeEventPattern, @@ -16,25 +16,25 @@ import { pipe, StateMachineTargetProps, } from "./target"; -import { EventTransformFunction, EventBusTransform } from "./transform"; -import { EventBusRuleInput } from "./types"; +import { EventTransformFunction, EventTransform } from "./transform"; +import { Event } from "./types"; /** * A function interface used by the {@link EventBus}'s when function to generate a rule. * * event is every event sent to the bus to be filtered. This argument is optional. */ -export type EventPredicateFunction = +export type RulePredicateFunction = | ((event: E) => event is O) | ((event: E) => boolean); -export interface IEventBusRule { +export interface IRule { readonly rule: aws_events.Rule; /** - * This static property identifies this class as an EventBusRule to the TypeScript plugin. + * This static property identifies this class as a Rule to the TypeScript plugin. */ - readonly functionlessKind: typeof EventBusRuleBase.FunctionlessType; + readonly functionlessKind: typeof RuleBase.FunctionlessType; /** * Transform the event before sending to a target using pipe using TargetInput transforms. @@ -104,10 +104,10 @@ export interface IEventBusRule { * Unsupported by Functionless: * * Variables from outside of the function scope */ - map

(transform: EventTransformFunction): EventBusTransform; + map

(transform: EventTransformFunction): EventTransform; /** - * Defines a target of the {@link EventBusTransform}'s rule using this TargetInput. + * Defines a target of the {@link EventTransform}'s rule using this TargetInput. * * The event is sent to the target verbatim unless .map is used first. * @@ -137,22 +137,21 @@ export interface IEventBusRule { * ``` */ pipe(props: LambdaTargetProps): void; - pipe(func: Function): void; - pipe(bus: EventBus): void; + pipe(func: IFunction): void; + pipe(bus: IEventBus): void; pipe(props: EventBusTargetProps): void; pipe(props: StateMachineTargetProps): void; pipe(props: StepFunction): void; pipe(props: ExpressStepFunction): void; + pipe(callback: () => aws_events.IRuleTarget): void; } -abstract class EventBusRuleBase - implements IEventBusRule -{ +abstract class RuleBase implements IRule { /** - * This static properties identifies this class as an EventBusRule to the TypeScript plugin. + * This static properties identifies this class as a Rule to the TypeScript plugin. */ - public static readonly FunctionlessType = "EventBusRule"; - readonly functionlessKind = "EventBusRule"; + public static readonly FunctionlessType = "Rule"; + readonly functionlessKind = "Rule"; _rule: aws_events.Rule | undefined = undefined; @@ -169,21 +168,24 @@ abstract class EventBusRuleBase /** * @inheritdoc */ - map

(transform: EventTransformFunction): EventBusTransform { - return new EventBusTransform(transform, this); + map

(transform: EventTransformFunction): EventTransform { + return new EventTransform(transform, this); } /** * @inheritdoc */ pipe(props: LambdaTargetProps): void; - pipe(func: Function): void; + pipe(func: IFunction): void; pipe(bus: IEventBus): void; pipe(props: EventBusTargetProps): void; pipe(props: StateMachineTargetProps): void; pipe(props: StepFunction): void; pipe(props: ExpressStepFunction): void; - pipe(resource: EventBusTargetResource): void { + pipe(callback: () => aws_events.IRuleTarget): void; + pipe( + resource: EventBusTargetResource | (() => aws_events.IRuleTarget) + ): void { pipe(this, resource as any); } } @@ -191,15 +193,15 @@ abstract class EventBusRuleBase /** * Special base rule that supports some internal behaviors like joining (AND) compiled rules. */ -export class EventBusPredicateRuleBase - extends EventBusRuleBase +export class PredicateRuleBase + extends RuleBase implements IEventBusFilterable { readonly document: PatternDocument; constructor( scope: Construct, id: string, - private bus: IEventBus, + private bus: IEventBus, /** * Functionless Pattern Document representation of Event Bridge rules. */ @@ -228,20 +230,41 @@ export class EventBusPredicateRuleBase /** * @inheritdoc */ - public when( + when( + id: string, + predicate: RulePredicateFunction + ): PredicateRuleBase; + when( scope: Construct, id: string, - predicate: EventPredicateFunction - ): EventBusPredicateRuleBase { - const document = synthesizePatternDocument(predicate as any); + predicate: RulePredicateFunction + ): PredicateRuleBase; + when( + scope: Construct | string, + id?: string | RulePredicateFunction, + predicate?: RulePredicateFunction + ): PredicateRuleBase { + if (predicate) { + const document = synthesizePatternDocument(predicate as any); + + return new PredicateRuleBase( + scope as Construct, + id as string, + this.bus as IEventBus, + this.document, + document + ); + } else { + const document = synthesizePatternDocument(id as any); - return new EventBusPredicateRuleBase( - scope, - id, - this.bus, - this.document, - document - ); + return new PredicateRuleBase( + this.bus.bus, + scope as string, + this.bus as IEventBus, + this.document, + document + ); + } } } @@ -251,34 +274,37 @@ export class EventBusPredicateRuleBase * * @see EventBus.when for more details on filtering events. */ -export class EventBusRule< - T extends EventBusRuleInput, +export class Rule< + T extends Event, O extends T = T -> extends EventBusPredicateRuleBase { +> extends PredicateRuleBase { constructor( scope: Construct, id: string, - bus: IEventBus, - predicate: EventPredicateFunction + bus: IEventBus, + predicate: RulePredicateFunction ) { const document = synthesizePatternDocument(predicate as any); - super(scope, id, bus, document); + super(scope, id, bus as IEventBus, document); } /** * Import an {@link aws_events.Rule} wrapped with Functionless abilities. */ - public static fromRule( - rule: aws_events.Rule - ): IEventBusRule { - return new ImportedEventBusRule(rule); + public static fromRule(rule: aws_events.Rule): IRule { + return new ImportedRule(rule); } } -class ImportedEventBusRule< - T extends EventBusRuleInput -> extends EventBusRuleBase { +/** + * The event structure output for all scheduled events. + * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-run-lambda-schedule.html#eb-schedule-create-rule + */ +export interface ScheduledEvent + extends Event<{}, "Scheduled Event", "aws.events"> {} + +export class ImportedRule extends RuleBase { constructor(rule: aws_events.Rule) { super(() => rule); } diff --git a/src/event-bridge/target-input.ts b/src/event-bridge/target-input.ts index d851ae78..fc31dedc 100644 --- a/src/event-bridge/target-input.ts +++ b/src/event-bridge/target-input.ts @@ -181,7 +181,7 @@ export const synthesizeEventBridgeTargets = ( }; } throw Error( - "Addition operator is only supported to concatinate at least one string to another value." + "Addition operator is only supported to concatenate at least one string to another value." ); } else { throw Error(`Unsupported binary operator: ${expr.op}`); diff --git a/src/event-bridge/target.ts b/src/event-bridge/target.ts index d4c51c06..22e891a3 100644 --- a/src/event-bridge/target.ts +++ b/src/event-bridge/target.ts @@ -1,5 +1,4 @@ import { aws_events, aws_events_targets } from "aws-cdk-lib"; -import { assertNever } from "../assert"; import { IFunction, isFunction } from "../function"; import { ExpressStepFunction, @@ -7,8 +6,8 @@ import { StepFunction, } from "../step-function"; import { IEventBus, isEventBus } from "./event-bus"; -import { IEventBusRule } from "./rule"; -import { EventBusRuleInput } from "./types"; +import { IRule } from "./rule"; +import { Event } from "./types"; export type LambdaTargetProps

= { func: IFunction; @@ -18,11 +17,11 @@ const isLambdaTargetProps =

(props: any): props is LambdaTargetProps

=> { return "func" in props; }; -export type EventBusTargetProps

= { +export type EventBusTargetProps

= { bus: IEventBus

; } & aws_events_targets.EventBusProps; -const isEventBusTargetProps =

( +const isEventBusTargetProps =

( props: any ): props is EventBusTargetProps

=> { return "bus" in props; @@ -39,20 +38,21 @@ const isStateMachineTargetProps =

( return "machine" in props; }; -export type EventBusTargetResource = +export type EventBusTargetResource = | IFunction | LambdaTargetProps

| IEventBus | EventBusTargetProps | ExpressStepFunction | StepFunction - | StateMachineTargetProps

; + | StateMachineTargetProps

+ | ((targetInput?: aws_events.RuleTargetInput) => aws_events.IRuleTarget); /** * Add a target to the run based on the configuration given. */ -export function pipe( - rule: IEventBusRule, +export function pipe( + rule: IRule, resource: EventBusTargetResource, targetInput?: aws_events.RuleTargetInput ) { @@ -98,7 +98,8 @@ export function pipe( input: targetInput, }) ); + } else { + const target = resource(targetInput); + return rule.rule.addTarget(target); } - - assertNever(resource); } diff --git a/src/event-bridge/transform.ts b/src/event-bridge/transform.ts index 42edde5e..52a27058 100644 --- a/src/event-bridge/transform.ts +++ b/src/event-bridge/transform.ts @@ -2,18 +2,18 @@ import { aws_events } from "aws-cdk-lib"; import { FunctionDecl } from "../declaration"; import { IFunction } from "../function"; import { StepFunction, ExpressStepFunction } from "../step-function"; -import { IEventBusRule } from "./rule"; +import { IRule } from "./rule"; import { LambdaTargetProps, pipe, StateMachineTargetProps } from "./target"; import { synthesizeEventBridgeTargets } from "./target-input"; -import { EventBusRuleInput } from "./types"; +import { Event } from "./types"; /** - * A function interface used by the {@link EventBusRule}'s map function. + * A function interface used by the {@link Rule}'s map function. * * event is the event matched by the rule. This argument is optional. * $utils is a collection of built-in utilities wrapping EventBridge TargetInputs like contextual constants available to the transformer. */ -export type EventTransformFunction = ( +export type EventTransformFunction = ( event: E, $utils: EventTransformUtils ) => O; @@ -34,36 +34,38 @@ export interface EventTransformUtils { * Represents an event that has been transformed using Target Input Transformers. * https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html * - * @see EventBusRule.map for more details on transforming event details. + * @see Rule.map for more details on transforming event details. */ -export class EventBusTransform { +export class EventTransform { readonly targetInput: aws_events.RuleTargetInput; /** - * This static property identifies this class as an EventBusTransform to the TypeScript plugin. + * This static property identifies this class as an EventTransform to the TypeScript plugin. */ - public static readonly FunctionlessType = "EventBusTransform"; + public static readonly FunctionlessType = "EventTransform"; - constructor( - func: EventTransformFunction, - readonly rule: IEventBusRule - ) { + constructor(func: EventTransformFunction, readonly rule: IRule) { const decl = func as unknown as FunctionDecl; this.targetInput = synthesizeEventBridgeTargets(decl); } /** - * Defines a target of the {@link EventBusTransform}'s rule using this TargetInput. + * Defines a target of the {@link EventTransform}'s rule using this TargetInput. * * EventBus is not a valid pipe target for transformed events. * - * @see EventBusRule.pipe for more details on pipe. + * @see Rule.pipe for more details on pipe. */ pipe(props: LambdaTargetProps

): void; pipe(func: IFunction): void; pipe(props: StateMachineTargetProps

): void; pipe(props: StepFunction): void; pipe(props: ExpressStepFunction): void; + pipe( + callback: ( + targetInput: aws_events.RuleTargetInput + ) => aws_events.IRuleTarget + ): void; pipe( resource: | IFunction @@ -71,6 +73,7 @@ export class EventBusTransform { | StateMachineTargetProps

| StepFunction | ExpressStepFunction + | ((targetInput: aws_events.RuleTargetInput) => aws_events.IRuleTarget) ): void { pipe(this.rule, resource as any, this.targetInput); } diff --git a/src/event-bridge/types.ts b/src/event-bridge/types.ts index 1b5c06d4..8bb398f8 100644 --- a/src/event-bridge/types.ts +++ b/src/event-bridge/types.ts @@ -1,5 +1,5 @@ import { aws_events as functionless_event_bridge } from "aws-cdk-lib"; -export interface EventBusRuleInput< +export interface Event< T = any, DetailType extends string = string, Source extends string = string diff --git a/src/integration.ts b/src/integration.ts index 476df6b0..70a2177c 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -152,6 +152,10 @@ export class IntegrationImpl } } +export type IntegrationCall = { + kind: K; +} & F; + /** * Helper method which masks an {@link Integration} object as a function of any form. * @@ -173,8 +177,8 @@ export class IntegrationImpl */ export function makeIntegration( integration: Integration -): { kind: K } & F { - return integration as unknown as { kind: K } & F; +): IntegrationCall { + return integration as unknown as IntegrationCall; } /** diff --git a/src/step-function.ts b/src/step-function.ts index 68576f70..b3c2d9a2 100644 --- a/src/step-function.ts +++ b/src/step-function.ts @@ -23,12 +23,8 @@ import { } from "./asl"; import { assertDefined, assertNodeKind } from "./assert"; import { FunctionDecl, isFunctionDecl } from "./declaration"; -import { - EventBus, - EventBusPredicateRuleBase, - EventBusRule, -} from "./event-bridge"; -import { EventBusRuleInput } from "./event-bridge/types"; +import { EventBus, PredicateRuleBase, Rule } from "./event-bridge"; +import { Event } from "./event-bridge/types"; import { CallExpr, isComputedPropertyNameExpr, @@ -364,7 +360,7 @@ interface StepFunctionDetail { } interface StepFunctionStatusChangedEvent - extends EventBusRuleInput< + extends Event< StepFunctionDetail, "Step Functions Execution Status Change", "aws.states" @@ -618,10 +614,10 @@ abstract class BaseStepFunction< public onSucceeded( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -641,10 +637,10 @@ abstract class BaseStepFunction< public onFailed( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -664,10 +660,10 @@ abstract class BaseStepFunction< public onStarted( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -687,10 +683,10 @@ abstract class BaseStepFunction< public onTimedOut( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -710,10 +706,10 @@ abstract class BaseStepFunction< public onAborted( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -733,14 +729,14 @@ abstract class BaseStepFunction< /** * Create event bus rule that matches any status change on this machine. */ - public onStatusChanged( + onStatusChanged( scope: Construct, id: string - ): EventBusRule { + ): Rule { const bus = EventBus.default(this); // We are not able to use the nice "when" function here because we don't compile - return new EventBusPredicateRuleBase( + return new PredicateRuleBase( scope, id, bus, @@ -759,7 +755,7 @@ abstract class BaseStepFunction< /** * Grant the given identity permissions to start an execution of this state - * machine. + * Rule */ public grantStartExecution(identity: aws_iam.IGrantable): aws_iam.Grant { return aws_iam.Grant.addToPrincipal({ diff --git a/test-app/src/app.ts b/test-app/src/app.ts index 09e4861b..49aecdd4 100644 --- a/test-app/src/app.ts +++ b/test-app/src/app.ts @@ -1,8 +1,8 @@ -import { App, Stack } from "aws-cdk-lib"; -import * as appsync from "@aws-cdk/aws-appsync-alpha"; import path from "path"; +import * as appsync from "@aws-cdk/aws-appsync-alpha"; +import { App, Stack } from "aws-cdk-lib"; +import { EventBus, Event } from "functionless"; import { PeopleDatabase, Person } from "./people-db"; -import { EventBus, EventBusRuleInput } from "functionless"; import { PeopleEvents } from "./people-events"; export const app = new App(); @@ -60,7 +60,7 @@ interface MyEventDetails { value: string; } -interface MyEvent extends EventBusRuleInput {} +interface MyEvent extends Event {} new EventBus(stack, "bus") .when(stack, "aRule", (event) => event.detail.value === "hello") diff --git a/test-app/src/func-test.ts b/test-app/src/func-test.ts index 40b2b9e1..091b8e04 100644 --- a/test-app/src/func-test.ts +++ b/test-app/src/func-test.ts @@ -33,7 +33,7 @@ new Function(stack, "startworkflow", async () => { } console.log(result); // broadcast dynamic result message to all consumers - eventBus({ + eventBus.putEvents({ "detail-type": "workflowComplete", detail: { result, diff --git a/test-app/src/message-board.ts b/test-app/src/message-board.ts index 2e19213a..8b655595 100644 --- a/test-app/src/message-board.ts +++ b/test-app/src/message-board.ts @@ -17,7 +17,7 @@ import { StepFunction, Table, EventBus, - EventBusRuleInput, + Event, ExpressStepFunction, } from "functionless"; @@ -216,18 +216,14 @@ export const addComment = new AppsyncResolver< }); interface MessageDeletedEvent - extends EventBusRuleInput< + extends Event< { count: number }, "Delete-Message-Success", "MessageDeleter" > {} interface PostDeletedEvent - extends EventBusRuleInput< - { id: string }, - "Delete-Post-Success", - "MessageDeleter" - > {} + extends Event<{ id: string }, "Delete-Post-Success", "MessageDeleter"> {} const customDeleteBus = new EventBus( stack, @@ -261,7 +257,7 @@ const deleteWorkflow = new StepFunction<{ postId: string }, void>( }) ); - customDeleteBus({ + customDeleteBus.putEvents({ "detail-type": "Delete-Message-Success", source: "MessageDeleter", detail: { @@ -281,7 +277,7 @@ const deleteWorkflow = new StepFunction<{ postId: string }, void>( }, }); - customDeleteBus({ + customDeleteBus.putEvents({ "detail-type": "Delete-Post-Success", source: "MessageDeleter", detail: { @@ -349,8 +345,7 @@ interface Notification { message: string; } -interface TestDeleteEvent - extends EventBusRuleInput<{ postId: string }, "Delete", "test"> {} +interface TestDeleteEvent extends Event<{ postId: string }, "Delete", "test"> {} const sendNotification = new Function( stack, @@ -423,7 +418,7 @@ new Function( async () => { const result = func(); console.log(`function result: ${result}`); - customDeleteBus({ + customDeleteBus.putEvents({ "detail-type": "Delete-Post-Success", source: "MessageDeleter", detail: { @@ -449,7 +444,7 @@ new Function( }, }); const { bus } = b; - bus({ + bus.putEvents({ "detail-type": "Delete-Message-Success", detail: { count: 0 }, source: "MessageDeleter", diff --git a/test-app/src/people-events.ts b/test-app/src/people-events.ts index 86050c17..49073559 100644 --- a/test-app/src/people-events.ts +++ b/test-app/src/people-events.ts @@ -9,7 +9,7 @@ interface UserDetails { } interface UserEvent - extends functionless.EventBusRuleInput< + extends functionless.Event< UserDetails, // We can provide custom detail-types to match on "Create" | "Update" | "Delete" diff --git a/test/eventbus.localstack.test.ts b/test/eventbus.localstack.test.ts index fe33217f..cd17fe72 100644 --- a/test/eventbus.localstack.test.ts +++ b/test/eventbus.localstack.test.ts @@ -1,7 +1,7 @@ import { aws_dynamodb, CfnOutput } from "aws-cdk-lib"; // eslint-disable-next-line import/no-extraneous-dependencies import { EventBridge, DynamoDB } from "aws-sdk"; -import { $AWS, EventBus, EventBusRuleInput, StepFunction, Table } from "../src"; +import { $AWS, EventBus, Event, StepFunction, Table } from "../src"; import { clientConfig, localstackTestSuite } from "./localstack"; const EB = new EventBridge(clientConfig); @@ -38,13 +38,9 @@ localstackTestSuite("eventBusStack", (testResource) => { "Bus event starts step function and writes to dynamo", (parent) => { const addr = new CfnOutput(parent, "out", { value: "" }); - const bus = new EventBus>( - parent, - "bus", - { - eventBusName: addr.node.addr, - } - ); + const bus = new EventBus>(parent, "bus", { + eventBusName: addr.node.addr, + }); const table = new Table<{ id: string }, "id">( new aws_dynamodb.Table(parent, "table", { tableName: addr.node.addr + "table", diff --git a/test/eventbus.test.ts b/test/eventbus.test.ts index 0f66aa2e..536ad325 100644 --- a/test/eventbus.test.ts +++ b/test/eventbus.test.ts @@ -1,7 +1,14 @@ -import { aws_events, aws_lambda, Stack } from "aws-cdk-lib"; +import { + aws_events, + aws_events_targets, + aws_lambda, + Duration, + Stack, +} from "aws-cdk-lib"; import { ExpressStepFunction, StepFunction } from "../src"; -import { EventBus, EventBusRule, EventBusRuleInput } from "../src/event-bridge"; -import { EventBusTransform } from "../src/event-bridge/transform"; +import { EventBus, Rule, Event, ScheduledEvent } from "../src/event-bridge"; +import { synthesizeEventPattern } from "../src/event-bridge/event-pattern"; +import { EventTransform } from "../src/event-bridge/transform"; import { Function } from "../src/function"; let stack: Stack; @@ -23,7 +30,7 @@ test("new bus without wrapper", () => { test("new rule without when", () => { const bus = new EventBus(stack, "bus"); - const rule = new EventBusRule(stack, "rule", bus, (_event) => true); + const rule = new Rule(stack, "rule", bus, (_event) => true); expect(rule.rule._renderEventPattern()).toEqual({ source: [{ prefix: "" }] }); }); @@ -31,8 +38,8 @@ test("new rule without when", () => { test("new transform without map", () => { const bus = new EventBus(stack, "bus"); - const rule = new EventBusRule(stack, "rule", bus, (_event) => true); - const transform = new EventBusTransform((event) => event.source, rule); + const rule = new Rule(stack, "rule", bus, (_event) => true); + const transform = new EventTransform((event) => event.source, rule); expect(transform.targetInput.bind(rule.rule)).toEqual({ inputPath: "$.source", @@ -42,8 +49,8 @@ test("new transform without map", () => { test("rule from existing rule", () => { const awsRule = new aws_events.Rule(stack, "rule"); - const rule = EventBusRule.fromRule(awsRule); - const transform = new EventBusTransform((event) => event.source, rule); + const rule = Rule.fromRule(awsRule); + const transform = new EventTransform((event) => event.source, rule); expect(transform.targetInput.bind(rule.rule)).toEqual({ inputPath: "$.source", @@ -56,6 +63,21 @@ test("new bus with when", () => { expect(rule.rule._renderEventPattern()).toEqual({ source: [{ prefix: "" }] }); }); +test("when using auto-source", () => { + const bus = new EventBus(stack, "bus"); + bus.when("rule", () => true).pipe(bus); + + expect(bus.bus.node.tryFindChild("rule")).not.toBeUndefined(); +}); + +test("rule when using auto-source", () => { + const bus = new EventBus(stack, "bus"); + const rule1 = bus.when("rule", () => true); + rule1.when("rule2", () => true).pipe(bus); + + expect(bus.bus.node.tryFindChild("rule2")).not.toBeUndefined(); +}); + test("refine rule", () => { const rule = new EventBus(stack, "bus").when( stack, @@ -126,7 +148,7 @@ test("new bus with when map pipe function", () => { }); test("refined bus with when pipe function", () => { - const func = new Function( + const func = Function.fromFunction( aws_lambda.Function.fromFunctionArn(stack, "func", "") ); const rule = new EventBus(stack, "bus").when( @@ -200,7 +222,7 @@ test("new bus with when map pipe express step function", () => { test("new bus with when map pipe function props", () => { const busBus = new EventBus(stack, "bus"); - const func = new Function( + const func = Function.fromFunction( aws_lambda.Function.fromFunctionArn(stack, "func", "") ); @@ -222,6 +244,43 @@ test("new bus with when map pipe function props", () => { ).toEqual(10); }); +test("pipe escape hatch", () => { + const busBus = new EventBus(stack, "bus"); + + const func = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + + const rule = busBus.when(stack, "rule", () => true); + rule.pipe(() => new aws_events_targets.LambdaFunction(func.resource)); + + expect( + (rule.rule as any).targets[0] as aws_events.RuleTargetConfig + ).toHaveProperty("arn"); +}); + +test("pipe map escape hatch", () => { + const busBus = new EventBus(stack, "bus"); + + const func = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + + const rule = busBus + .when(stack, "rule", () => true) + .map((event) => event.source); + rule.pipe( + (targetInput) => + new aws_events_targets.LambdaFunction(func.resource, { + event: targetInput, + }) + ); + + expect( + (rule.rule.rule as any).targets[0] as aws_events.RuleTargetConfig + ).toHaveProperty("arn"); +}); + interface t1 { type: "one"; one: string; @@ -232,7 +291,7 @@ interface t2 { two: string; } -interface tt extends EventBusRuleInput {} +interface tt extends Event {} test("when narrows type to map", () => { const bus = EventBus.default(stack); @@ -241,7 +300,7 @@ test("when narrows type to map", () => { .when( stack, "rule", - (event): event is EventBusRuleInput => event.detail.type === "one" + (event): event is Event => event.detail.type === "one" ) .map((event) => event.detail.one); }); @@ -253,13 +312,13 @@ test("when narrows type to map", () => { .when( stack, "rule", - (event): event is EventBusRuleInput => event.detail.type === "two" + (event): event is Event => event.detail.type === "two" ) .when(stack, "rule2", (event) => event.detail.two === "something"); }); test("map narrows type and pipe enforces", () => { - const lambda = new Function( + const lambda = Function.fromFunction( aws_lambda.Function.fromFunctionArn(stack, "func", "") ); const bus = EventBus.default(stack); @@ -268,9 +327,97 @@ test("map narrows type and pipe enforces", () => { .when( stack, "rule", - (event): event is EventBusRuleInput => event.detail.type === "one" + (event): event is Event => event.detail.type === "one" ) .map((event) => event.detail.one) // should fail compilation if the types don't match .pipe(lambda); }); + +test("a scheduled rule can be mapped and pipped", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus + .schedule(stack, "rule", aws_events.Schedule.rate(Duration.hours(1))) + .map((event) => event.id) + // should fail compilation if the types don't match + .pipe(lambda); +}); + +test("a scheduled rule can be pipped", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus + .schedule(stack, "rule", aws_events.Schedule.rate(Duration.hours(1))) + .pipe(lambda); +}); + +test("when any", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus + .all() + .map((event) => event.id) + // should fail compilation if the types don't match + .pipe(lambda); + + const rule = bus.bus.node.tryFindChild("all"); + expect(rule).not.toBeUndefined(); +}); + +test("when any pipe", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus.all().pipe(lambda); + + const rule = bus.bus.node.tryFindChild("all"); + expect(rule).not.toBeUndefined(); +}); + +test("when any multiple times does not create new rules", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus.all().pipe(lambda); + bus.all().pipe(lambda); + bus.all().pipe(lambda); + + const rule = bus.bus.node.tryFindChild("all"); + expect(rule).not.toBeUndefined(); +}); + +test("when any pipe", () => { + const lambda = Function.fromFunction( + aws_lambda.Function.fromFunctionArn(stack, "func", "") + ); + const bus = EventBus.default(stack); + + bus.all(stack, "anyRule").pipe(lambda); + + const rule = stack.node.tryFindChild("anyRule"); + expect(rule).not.toBeUndefined(); +}); + +test("when any when pipe", () => { + const bus = EventBus.default(stack); + + const rule = bus + .all(stack, "anyRule") + .when("rule1", (event) => event.id === "test"); + + expect(synthesizeEventPattern(rule.document)).toEqual({ id: ["test"] }); +}); diff --git a/test/eventpattern.test.ts b/test/eventpattern.test.ts index ea4c6674..a4eede60 100644 --- a/test/eventpattern.test.ts +++ b/test/eventpattern.test.ts @@ -1,8 +1,8 @@ import { reflect } from "../src"; -import { EventBusRuleInput, EventPredicateFunction } from "../src/event-bridge"; +import { Event, RulePredicateFunction } from "../src/event-bridge"; import { ebEventPatternTestCase, ebEventPatternTestCaseError } from "./util"; -type TestEvent = EventBusRuleInput<{ +type TestEvent = Event<{ num: number; str: string; optional?: string; @@ -21,7 +21,7 @@ describe("event pattern", () => { describe("equals", () => { test("simple", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.source === "lambda" ), { @@ -32,7 +32,7 @@ describe("event pattern", () => { test("no event parameter", () => { ebEventPatternTestCase( - reflect>(() => true), + reflect>(() => true), { source: [{ prefix: "" }], } @@ -41,7 +41,7 @@ describe("event pattern", () => { test("index access", () => { ebEventPatternTestCase( - reflect>( + reflect>( // eslint-disable-next-line dot-notation (event) => event["source"] === "lambda" ), @@ -53,7 +53,7 @@ describe("event pattern", () => { test("double equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.source == "lambda" ), { @@ -64,7 +64,7 @@ describe("event pattern", () => { test("is null", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.source === null ), { @@ -75,7 +75,7 @@ describe("event pattern", () => { test("detail", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "something" ), { @@ -86,7 +86,7 @@ describe("event pattern", () => { test("detail deep", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.deep.value === "something" ), { @@ -97,7 +97,7 @@ describe("event pattern", () => { test("detail index accessor", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail["multi-part"] === "something" ), { @@ -108,7 +108,7 @@ describe("event pattern", () => { test("numeric", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === 50 ), { @@ -119,7 +119,7 @@ describe("event pattern", () => { test("negative number", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === -50 ), { @@ -130,9 +130,7 @@ describe("event pattern", () => { test("boolean implicit", () => { ebEventPatternTestCase( - reflect>( - (event) => event.detail.bool - ), + reflect>((event) => event.detail.bool), { detail: { bool: [true] }, } @@ -141,7 +139,7 @@ describe("event pattern", () => { test("boolean implicit false", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.bool ), { @@ -152,7 +150,7 @@ describe("event pattern", () => { test("boolean explicit", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.bool === false ), { @@ -164,7 +162,7 @@ describe("event pattern", () => { describe("array", () => { test("array", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.detail.array.includes("something") ), { @@ -175,7 +173,7 @@ describe("event pattern", () => { test("num array", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.detail.numArray.includes(1) ), { @@ -186,7 +184,7 @@ describe("event pattern", () => { test("num array", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.detail.numArray.includes(-1) ), { @@ -197,7 +195,7 @@ describe("event pattern", () => { test("bool array", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.detail.boolArray.includes(true) ), { @@ -208,7 +206,7 @@ describe("event pattern", () => { test("array not includes", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.array.includes("something") ), { @@ -219,7 +217,7 @@ describe("event pattern", () => { test("num array not includes", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.numArray.includes(1) ), { @@ -230,7 +228,7 @@ describe("event pattern", () => { test("bool array not includes", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.boolArray.includes(true) ), { @@ -241,7 +239,7 @@ describe("event pattern", () => { test("array explicit equals error", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.array === ["a", "b"] ), "Event Patterns can only compare primitive values" @@ -253,7 +251,7 @@ describe("event pattern", () => { describe("prefix", () => { test("prefix", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.source.startsWith("l") ), { @@ -264,7 +262,7 @@ describe("event pattern", () => { test("not prefix", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.source.startsWith("l") ), { @@ -275,7 +273,7 @@ describe("event pattern", () => { test("prefix non string", () => { ebEventPatternTestCaseError( - reflect>((event) => + reflect>((event) => (event.detail.num).startsWith("l") ), "Starts With operation only supported on strings, found number." @@ -286,7 +284,7 @@ describe("event pattern", () => { describe("numeric range single", () => { test("numeric range single", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num < 100 ), { @@ -297,7 +295,7 @@ describe("event pattern", () => { test("numeric range greater than", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num > 100 ), { @@ -308,7 +306,7 @@ describe("event pattern", () => { test("numeric range greater than equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num >= 100 ), { @@ -319,7 +317,7 @@ describe("event pattern", () => { test("numeric range inverted", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => 100 < event.detail.num ), { @@ -330,7 +328,7 @@ describe("event pattern", () => { test("numeric range inverted", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => 100 >= event.detail.num ), { @@ -343,7 +341,7 @@ describe("event pattern", () => { describe("not", () => { test("string", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.source !== "lambda" ), { @@ -354,7 +352,7 @@ describe("event pattern", () => { test("not not prefix", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!event.source.startsWith("lambda") ), { @@ -365,7 +363,7 @@ describe("event pattern", () => { test("negate string equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.source === "lambda") ), { @@ -376,7 +374,7 @@ describe("event pattern", () => { test("negate not string equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.source !== "lambda") ), { @@ -387,7 +385,7 @@ describe("event pattern", () => { test("not bool true", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.bool !== true ), { @@ -398,7 +396,7 @@ describe("event pattern", () => { test("negate not bool true", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.detail.bool !== true) ), { @@ -409,7 +407,7 @@ describe("event pattern", () => { test("negate bool true", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.detail.bool === true) ), { @@ -420,7 +418,7 @@ describe("event pattern", () => { test("string wrapped", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.source === "lambda") ), { @@ -431,7 +429,7 @@ describe("event pattern", () => { test("number", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num !== 100 ), { @@ -442,7 +440,7 @@ describe("event pattern", () => { test("array", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.array.includes("something") ), { @@ -453,7 +451,7 @@ describe("event pattern", () => { test("string prefix", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.str.startsWith("something") ), { @@ -464,7 +462,7 @@ describe("event pattern", () => { test("negate multiple fields", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !(event.detail.str === "hello" && event.id === "there") ), "Can only negate simple statements like equals, doesn't equals, and prefix." @@ -473,7 +471,7 @@ describe("event pattern", () => { test("negate multiple", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !(event.detail.str || !event.detail.str) ), "Impossible logic discovered." @@ -482,7 +480,7 @@ describe("event pattern", () => { test("negate negate multiple", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!(event.detail.str || !event.detail.str) ), { source: [{ prefix: "" }] } @@ -491,7 +489,7 @@ describe("event pattern", () => { test("negate intrafield valid aggregate", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !(event.detail.str.startsWith("hello") || event.detail.str === "hi") ) @@ -503,7 +501,7 @@ describe("event pattern", () => { describe("exists", () => { test("does", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.optional !== undefined ), { @@ -514,7 +512,7 @@ describe("event pattern", () => { test("does in", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => "optional" in event.detail ), { @@ -525,7 +523,7 @@ describe("event pattern", () => { test("does exist lone value", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!event.detail.optional ), { @@ -536,7 +534,7 @@ describe("event pattern", () => { test("does not", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.optional === undefined ), { @@ -547,7 +545,7 @@ describe("event pattern", () => { test("does not in", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !("optional" in event.detail) ), { @@ -558,9 +556,7 @@ describe("event pattern", () => { test("exists at event level", () => { ebEventPatternTestCase( - reflect>( - (event) => "source" in event - ), + reflect>((event) => "source" in event), { source: [{ exists: true }], } @@ -572,7 +568,7 @@ describe("event pattern", () => { const myConstant = "hello"; test("external constant", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === myConstant ), { @@ -585,7 +581,7 @@ describe("event pattern", () => { test("external constant", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === constantObj.value ), { @@ -596,7 +592,7 @@ describe("event pattern", () => { test("internal constant", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const myInternalContant = "hi"; return event.detail.str === myInternalContant; }), @@ -608,7 +604,7 @@ describe("event pattern", () => { test("formatting", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const myInternalContant = "hi"; return event.detail.str === `${myInternalContant} there`; }), @@ -620,7 +616,7 @@ describe("event pattern", () => { test("internal property", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const myInternalContant = { value: "hi" }; return event.detail.str === myInternalContant.value; }), @@ -632,7 +628,7 @@ describe("event pattern", () => { test("constant function call", () => { ebEventPatternTestCaseError( - reflect>((event) => { + reflect>((event) => { const myInternalContant = (() => "hi" + " " + "there")(); return event.detail.str === myInternalContant; }), @@ -642,7 +638,7 @@ describe("event pattern", () => { test("constant function call", () => { ebEventPatternTestCaseError( - reflect>((event) => { + reflect>((event) => { const myMethod = () => "hi" + " " + "there"; return event.detail.str === myMethod(); }), @@ -654,7 +650,7 @@ describe("event pattern", () => { describe("simple invalid", () => { test("error on raw event in predicate", () => { ebEventPatternTestCaseError( - reflect>((event) => !!event), + reflect>((event) => !!event), "Identifier is unsupported" ); }); @@ -663,7 +659,7 @@ describe("event pattern", () => { describe("numeric aggregate", () => { test("numeric range aggregate", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 ), { @@ -674,7 +670,7 @@ describe("event pattern", () => { test("numeric range overlapping", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -688,7 +684,7 @@ describe("event pattern", () => { test("numeric range negate", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.detail.num >= 100 && event.detail.num < 1000) ), { @@ -699,7 +695,7 @@ describe("event pattern", () => { test("numeric range overlapping", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -713,7 +709,7 @@ describe("event pattern", () => { test("numeric range aggregate with other fields", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -730,7 +726,7 @@ describe("event pattern", () => { test("numeric range or exclusive", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num > 300 || event.detail.num < 200 ), { @@ -741,7 +737,7 @@ describe("event pattern", () => { test("numeric range or exclusive negate", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(event.detail.num > 300 || event.detail.num < 200) ), { @@ -753,7 +749,7 @@ describe("event pattern", () => { // the ranges represent infinity, so the clause is removed test("numeric range or aggregate empty", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num < 300 || event.detail.num > 200 ), { @@ -764,7 +760,7 @@ describe("event pattern", () => { test("numeric range or aggregate", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 && event.detail.num < 350) || event.detail.num < 200 @@ -779,7 +775,7 @@ describe("event pattern", () => { test("numeric range or and AND", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 || event.detail.num < 200) && event.detail.num > 0 @@ -802,7 +798,7 @@ describe("event pattern", () => { */ test("numeric range or and AND part reduced", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 || event.detail.num < 200) && (event.detail.num > 0 || event.detail.num < 500) @@ -817,7 +813,7 @@ describe("event pattern", () => { test("numeric range or and AND part reduced inverted", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 0 || event.detail.num < 500) && (event.detail.num > 300 || event.detail.num < 200) @@ -832,7 +828,7 @@ describe("event pattern", () => { test("numeric range or and AND part reduced both valid", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 || event.detail.num < 200) && (event.detail.num > 250 || event.detail.num < 100) @@ -847,7 +843,7 @@ describe("event pattern", () => { test("numeric range or and AND part reduced both ranges", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => ((event.detail.num >= 10 && event.detail.num <= 20) || (event.detail.num >= 30 && event.detail.num <= 40)) && @@ -864,7 +860,7 @@ describe("event pattern", () => { test("numeric range or and AND part reduced both losing some range", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => ((event.detail.num >= 10 && event.detail.num <= 20) || (event.detail.num >= 30 && event.detail.num <= 40)) && @@ -881,7 +877,7 @@ describe("event pattern", () => { test("numeric range multiple distinct segments", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 && event.detail.num < 400) || (event.detail.num > 0 && event.detail.num < 200) || @@ -901,7 +897,7 @@ describe("event pattern", () => { test("numeric range multiple distinct segments overlapped", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 && event.detail.num < 400) || (event.detail.num > 0 && event.detail.num < 200) || @@ -918,7 +914,7 @@ describe("event pattern", () => { test("numeric range multiple distinct segments merged", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 && event.detail.num < 400) || (event.detail.num > 0 && event.detail.num < 200) || @@ -938,7 +934,7 @@ describe("event pattern", () => { test("numeric range or and AND dropped range", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.num > 300 || event.detail.num < 200) && event.detail.num > 400 @@ -953,7 +949,7 @@ describe("event pattern", () => { test("numeric range nil range error upper", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -965,7 +961,7 @@ describe("event pattern", () => { test("numeric range nil range OR valid range", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === 10 || (event.detail.num >= 100 && @@ -982,7 +978,7 @@ describe("event pattern", () => { test("numeric range nil range AND invalid range", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -995,7 +991,7 @@ describe("event pattern", () => { test("numeric range nil range OR valid aggregate range", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === 10 || event.detail.num === 11 || @@ -1013,7 +1009,7 @@ describe("event pattern", () => { test("numeric range nil range error lower", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 1000 && @@ -1024,7 +1020,7 @@ describe("event pattern", () => { test("numeric range nil range illogical", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.num >= 100 && event.detail.num < 50 ), "Found zero range numeric range lower 100 inclusive: true, upper 50 inclusive: false" @@ -1033,7 +1029,7 @@ describe("event pattern", () => { test("numeric range nil range illogical with override", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === 10 || (event.detail.num >= 100 && event.detail.num < 50) @@ -1050,7 +1046,7 @@ describe("event pattern", () => { describe("aggregate", () => { test("test for optional", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!event.detail.optional && event.detail.optional === "value" ), @@ -1064,7 +1060,7 @@ describe("event pattern", () => { test("test for optional number", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!event.detail.optionalNum && event.detail.optionalNum > 10 && @@ -1080,7 +1076,7 @@ describe("event pattern", () => { test("number and string separate fields", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.num === 10 && event.detail.str === "hello" ), { @@ -1094,7 +1090,7 @@ describe("event pattern", () => { test("same field AND string", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.str === "hi" && event.detail.str === "hello" ), @@ -1104,7 +1100,7 @@ describe("event pattern", () => { test("same field AND string with OR", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "huh" || (event.detail.str === "hi" && event.detail.str === "hello") @@ -1119,7 +1115,7 @@ describe("event pattern", () => { test("same field AND string identical", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hi" && event.detail.str === "hi" ), { @@ -1132,7 +1128,7 @@ describe("event pattern", () => { test("same field OR string", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hi" || event.detail.str === "hello" ), { @@ -1145,7 +1141,7 @@ describe("event pattern", () => { test("same field OR string and AND another field ", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => (event.detail.str === "hi" || event.detail.str === "hello") && event.detail.num === 100 @@ -1161,7 +1157,7 @@ describe("event pattern", () => { test("same field AND another field ", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hello" && event.detail.num === 100 ), { @@ -1175,7 +1171,7 @@ describe("event pattern", () => { test("same field || another field ", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.str === "hello" || event.detail.num === 100 ), "Event bridge does not support OR logic between multiple fields, found str and num." @@ -1184,7 +1180,7 @@ describe("event pattern", () => { test("lots of AND", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hi" && event.detail.num === 100 && @@ -1206,7 +1202,7 @@ describe("event pattern", () => { test("AND prefix", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str.startsWith("hi") && event.detail.num === 100 ), @@ -1221,7 +1217,7 @@ describe("event pattern", () => { test("AND not prefix", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !event.detail.str.startsWith("hi") && event.detail.str !== "hello" ), @@ -1231,7 +1227,7 @@ describe("event pattern", () => { test("AND not prefix reverse", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.detail.str !== "hello" && !event.detail.str.startsWith("hi") ), @@ -1241,7 +1237,7 @@ describe("event pattern", () => { test("AND not two prefix", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !event.detail.str.startsWith("hello") && !event.detail.str.startsWith("hi") @@ -1252,7 +1248,7 @@ describe("event pattern", () => { test("AND list", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.array.includes("hi") && event.detail.num === 100 ), @@ -1267,7 +1263,7 @@ describe("event pattern", () => { test("AND exists", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !!event.detail.optional && event.detail.num === 100 ), { @@ -1281,7 +1277,7 @@ describe("event pattern", () => { test("AND not exists", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.optional && event.detail.num === 100 ), { @@ -1295,7 +1291,7 @@ describe("event pattern", () => { test("AND not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str !== "hi" && event.detail.str !== "hello" ), { @@ -1308,7 +1304,7 @@ describe("event pattern", () => { test("AND not not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( // str === "hi" || str === "hello" (event) => !(event.detail.str !== "hi" && event.detail.str !== "hello") @@ -1323,7 +1319,7 @@ describe("event pattern", () => { test("AND not exists and exists impossible", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !event.detail.str && event.detail.str ), "Field cannot both be present and not present." @@ -1332,7 +1328,7 @@ describe("event pattern", () => { test("AND not exists and exists impossible", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hi" || (!event.detail.str && event.detail.str) @@ -1347,7 +1343,7 @@ describe("event pattern", () => { test("AND not exists and not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.str && event.detail.str !== "x" ), { @@ -1360,7 +1356,7 @@ describe("event pattern", () => { test("AND not exists and value", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => !event.detail.str && event.detail.str === "x" ), "Invalid comparison: pattern cannot both be not present as a positive value" @@ -1369,7 +1365,7 @@ describe("event pattern", () => { test("AND not exists and value", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str === "hello" || (!event.detail.str && event.detail.str === "x") @@ -1384,7 +1380,7 @@ describe("event pattern", () => { test("AND not null and not value", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str !== null && event.detail.str !== "x" ), { @@ -1397,7 +1393,7 @@ describe("event pattern", () => { test("AND not not exists and not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !(!event.detail.str && event.detail.str !== "x") ), { @@ -1410,7 +1406,7 @@ describe("event pattern", () => { test("AND not exists and not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str !== "x" && !event.detail.str ), { @@ -1423,7 +1419,7 @@ describe("event pattern", () => { test("OR not equals", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str !== "hi" || event.detail.str !== "hello" ), @@ -1433,7 +1429,7 @@ describe("event pattern", () => { test("AND not eq", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => event.detail.str !== "hi" && event.detail.num === 100 ), { @@ -1447,7 +1443,7 @@ describe("event pattern", () => { test("OR not exists", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.optional || event.detail.optional === "cheese" ), @@ -1461,7 +1457,7 @@ describe("event pattern", () => { test("OR not exists not eq", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.optional || event.detail.optional !== "cheese" ), @@ -1475,7 +1471,7 @@ describe("event pattern", () => { test("OR not exists starts with", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.optional || event.detail.optional.startsWith("cheese") ), @@ -1489,7 +1485,7 @@ describe("event pattern", () => { test("OR not exists not starts with", () => { ebEventPatternTestCase( - reflect>( + reflect>( (event) => !event.detail.optional || !event.detail.optional.startsWith("cheese") @@ -1509,14 +1505,14 @@ describe("event pattern", () => { describe("error edge cases", () => { test("comparing non event values", () => { ebEventPatternTestCaseError( - reflect>((_event) => "10" === "10"), + reflect>((_event) => "10" === "10"), "Expected exactly one event reference, got zero." ); }); test("comparing two event values", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( (event) => event.id === event.region ), "Expected exactly one event reference, got two." @@ -1529,7 +1525,7 @@ describe("event pattern", () => { describe.skip("destructure", () => { test("destructure parameter", () => { ebEventPatternTestCase( - reflect>( + reflect>( ({ source }) => source === "lambda" ), { @@ -1540,7 +1536,7 @@ describe.skip("destructure", () => { test("destructure variable", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const { source } = event; return source === "lambda"; }), @@ -1552,7 +1548,7 @@ describe.skip("destructure", () => { test("destructure multi-layer variable", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const { detail: { str }, } = event; @@ -1566,7 +1562,7 @@ describe.skip("destructure", () => { test("destructure array doesn't work", () => { ebEventPatternTestCaseError( - reflect>((event) => { + reflect>((event) => { const { detail: { array: [value], @@ -1579,7 +1575,7 @@ describe.skip("destructure", () => { test("destructure parameter array doesn't work", () => { ebEventPatternTestCaseError( - reflect>( + reflect>( ({ detail: { array: [value], @@ -1591,7 +1587,7 @@ describe.skip("destructure", () => { test("descture variable rename", () => { ebEventPatternTestCase( - reflect>((event) => { + reflect>((event) => { const { source: src } = event; return src === "lambda"; }), @@ -1603,7 +1599,7 @@ describe.skip("destructure", () => { test("destructure parameter rename", () => { ebEventPatternTestCase( - reflect>( + reflect>( ({ source: src }) => src === "lambda" ), { @@ -1617,7 +1613,7 @@ describe.skip("destructure", () => { describe.skip("list some", () => { test("list starts with", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.resources.some((r) => r.startsWith("hi")) ), { @@ -1628,7 +1624,7 @@ describe.skip("list some", () => { test("list starts with AND errors", () => { ebEventPatternTestCaseError( - reflect>((event) => + reflect>((event) => event.resources.some((r) => r.startsWith("hi") && r === "taco") ) ); @@ -1636,7 +1632,7 @@ describe.skip("list some", () => { test("list starts with OR is fine", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.resources.some( (r) => r.startsWith("hi") || r.startsWith("taco") || r === "cheddar" ) @@ -1649,7 +1645,7 @@ describe.skip("list some", () => { test("list some instead of includes", () => { ebEventPatternTestCase( - reflect>((event) => + reflect>((event) => event.resources.some((r) => r === "cheddar") ), { diff --git a/test/eventtargets.test.ts b/test/eventtargets.test.ts index 422a3320..30c6b6bf 100644 --- a/test/eventtargets.test.ts +++ b/test/eventtargets.test.ts @@ -1,11 +1,11 @@ import { aws_events, Stack } from "aws-cdk-lib"; import { EventField } from "aws-cdk-lib/aws-events"; import { Function, reflect, StepFunction } from "../src"; -import { EventBusRuleInput } from "../src/event-bridge"; +import { Event } from "../src/event-bridge"; import { ebEventTargetTestCase, ebEventTargetTestCaseError } from "./util"; -type testEvent = EventBusRuleInput<{ +type testEvent = Event<{ value: string; optional?: string; num: number; @@ -251,7 +251,7 @@ test("object with bare undefined", () => { }); type MyString = string; -interface MyTest extends EventBusRuleInput<{ s: MyString }> {} +interface MyTest extends Event<{ s: MyString }> {} test("non-string type", () => { ebEventTargetTestCase( @@ -663,7 +663,7 @@ describe("not allowed", () => { test("math", () => { ebEventTargetTestCaseError( reflect((event) => event.detail.num + 1), - "Addition operator is only supported to concatinate at least one string to another value." + "Addition operator is only supported to concatenate at least one string to another value." ); }); diff --git a/test/function.localstack.test.ts b/test/function.localstack.test.ts index 1577ad18..0fb41611 100644 --- a/test/function.localstack.test.ts +++ b/test/function.localstack.test.ts @@ -12,7 +12,7 @@ import { Construct } from "constructs"; import { $AWS, EventBus, - EventBusRuleInput, + Event, ExpressStepFunction, Function, FunctionProps, @@ -345,7 +345,7 @@ localstackTestSuite("functionStack", (testResource, _stack, _app) => { "function", localstackClientConfig, async () => { - bus({ + bus.putEvents({ "detail-type": "detail", source: "lambda", detail: {}, @@ -394,7 +394,7 @@ localstackTestSuite("functionStack", (testResource, _stack, _app) => { testFunctionResource.skip( "Call Lambda AWS SDK put event to bus without reference", (parent) => { - const bus = new EventBus(parent, "bus"); + const bus = new EventBus(parent, "bus"); return new Function( parent, @@ -421,14 +421,14 @@ localstackTestSuite("functionStack", (testResource, _stack, _app) => { testFunctionResource( "Call Lambda AWS SDK put event to bus with in closure reference", (parent) => { - const bus = new EventBus(parent, "bus"); + const bus = new EventBus(parent, "bus"); return new Function( parent, "function", localstackClientConfig, async () => { const busbus = bus; - busbus({ + busbus.putEvents({ "detail-type": "anyDetail", source: "anySource", detail: {}, @@ -442,14 +442,14 @@ localstackTestSuite("functionStack", (testResource, _stack, _app) => { testFunctionResource( "Call Lambda AWS SDK integration from destructured object aa", (parent) => { - const buses = { bus: new EventBus(parent, "bus") }; + const buses = { bus: new EventBus(parent, "bus") }; return new Function( parent, "function", localstackClientConfig, async () => { const { bus } = buses; - bus({ + bus.putEvents({ "detail-type": "anyDetail", source: "anySource", detail: {}, diff --git a/test/step-function.test.ts b/test/step-function.test.ts index 91f8e104..0a613ddc 100644 --- a/test/step-function.test.ts +++ b/test/step-function.test.ts @@ -4,7 +4,7 @@ import { $AWS, $SFN, EventBus, - EventBusRuleInput, + Event, ExpressStepFunction, StepFunction, SyncExecutionResult, @@ -1067,7 +1067,7 @@ test("put an event bus event", () => { interface BusDetails { value: string; } - interface BusEvent extends EventBusRuleInput {} + interface BusEvent extends Event {} const bus = new EventBus(stack, "testBus2"); @@ -1075,7 +1075,7 @@ test("put an event bus event", () => { stack, "fn", (input) => { - bus({ + bus.putEvents({ "detail-type": "someEvent", source: "sfnTest", detail: { @@ -1087,9 +1087,9 @@ test("put an event bus event", () => { expect(definition).toEqual({ StartAt: - 'bus({detail-type: "someEvent", source: "sfnTest", detail: {value: input.id}', + 'bus.putEvents({detail-type: "someEvent", source: "sfnTest", detail: {value:', States: { - 'bus({detail-type: "someEvent", source: "sfnTest", detail: {value: input.id}': + 'bus.putEvents({detail-type: "someEvent", source: "sfnTest", detail: {value:': { Type: "Task", Resource: "arn:aws:states:::events:putEvents", @@ -1125,7 +1125,7 @@ test("put multiple event bus events", () => { value: string; constant?: string; } - interface BusEvent extends EventBusRuleInput {} + interface BusEvent extends Event {} const bus = new EventBus(stack, "testBus"); @@ -1133,7 +1133,7 @@ test("put multiple event bus events", () => { stack, "fn", (input) => { - bus( + bus.putEvents( { "detail-type": "someEvent", source: "sfnTest", @@ -1155,9 +1155,9 @@ test("put multiple event bus events", () => { expect(definition).toEqual({ StartAt: - 'bus({detail-type: "someEvent", source: "sfnTest", detail: {value: input.id}', + 'bus.putEvents({detail-type: "someEvent", source: "sfnTest", detail: {value:', States: { - 'bus({detail-type: "someEvent", source: "sfnTest", detail: {value: input.id}': + 'bus.putEvents({detail-type: "someEvent", source: "sfnTest", detail: {value:': { Type: "Task", Resource: "arn:aws:states:::events:putEvents", diff --git a/test/util.ts b/test/util.ts index eeadc41b..caf5a0f7 100644 --- a/test/util.ts +++ b/test/util.ts @@ -16,7 +16,7 @@ import { FunctionDecl, Table, Function, - EventBusRuleInput, + Event, FunctionlessEventPattern, } from "../src"; @@ -170,7 +170,7 @@ beforeEach(() => { stack = new Stack(); }); -export function ebEventTargetTestCase( +export function ebEventTargetTestCase( decl: FunctionDecl> | Err, targetInput: aws_events.RuleTargetInput ) { @@ -202,7 +202,7 @@ export function ebEventTargetTestCase( }); } -export function ebEventTargetTestCaseError( +export function ebEventTargetTestCaseError( decl: FunctionDecl> | Err, message?: string ) { diff --git a/website/docs/concepts/event-bridge/event-bus.md b/website/docs/concepts/event-bridge/event-bus.md index f2cd7d37..04b4f1a9 100644 --- a/website/docs/concepts/event-bridge/event-bus.md +++ b/website/docs/concepts/event-bridge/event-bus.md @@ -6,343 +6,63 @@ sidebar_position: 1 An `EventBus` ingests and routes events throughout your application. Events can come from and be sent to any service, including AWS services or non-AWS SaaS such as Slack, Stripe, etc. -## Create an EventBus - -Import the `EventBus` Construct and instantiate it. +## New Event Bus ```ts import { EventBus } from "functionless"; -const bus = new EventBus(scope, "Bus"); -``` - -## Declare the types of Events - -It is recommended to declare types for each of the event types flowing through an `EventBus`. - -First, declare an `interface` representing an Event's payload. - -```ts -interface UserDetails { - id?: string; - name: string; - age: number; - interests: string[]; -} -``` - -Then, declare an `interface` for the Event's envelope - i.e. the shape of the data - -```ts -interface UserEvent - extends functionless.EventBusRuleInput< - UserDetails, - // We can provide custom detail-types to match on - "Create" | "Update" | "Delete" - > {} -``` - -## Filter Events with an EventBusRule - -An `EventBusRule` filters events flowing through an `EventBus` using some conditional expression. - -```ts -const onSignUp = bus.when( - scope, - "OnSignUp", - (event) => event["detail-type"] === "SignUp" -); -``` - -See the [Syntax Guide for Event Patterns](./syntax.md#event-patterns) documentation for a detailed reference on the syntax supported within an `EventBusRule`. - -## Transform an Event - -The `map` function can be applied to an `EventBusRule` to transform the structure of the event before integration. +new EventBus(stack, "bus"); -```ts -const onSignUpTransformed = onSignUp.map((event) => ({ - type: "SignUp", - ...event, -})); +// to name the bus, use props +new EventBus(stack, "bus2", { eventBusName: "myBus" }); ``` -See the [Syntax Guide for Event Transforms](./syntax.md#event-transforms) documentation for a detailed reference on the syntax supported within an `EventBusTransform`. - -## Pipe an Event to an Integration - -The `pipe` function can be applied to an `EventBusRule` or `EventBusTransform` to send events on to an Integration, such as a Lambda `Function`, `StepFunction` or another `EventBus`. +## Default Bus -```ts -const processSignUpEvent = new Function( - scope, - "ProcessSignUp", - async (event: OnSignUp) => { - await table.putItem({ - item: event, - }); - } -); - -onSignUp.pipe(processSignUpEvent); -``` - -## Declare an Event Type +There is a default Event Bus in every region of an AWS account. It contains events emitted by your AWS Resources, such as when a Step Function execution completes (see [Event Sources](./event-sources#resources-with-event-sources)), or when a scheduled trigger fires. -Functionless supports well typed events, lets add our event schema to Typescript. +Functionless provides an easy way to work with the default bus. ```ts -interface UserDetails { - id?: string; - name: string; - age: number; - interests: string[]; -} - -interface UserEvent - extends functionless.EventBusRuleInput< - UserDetails, - // We can provide custom detail-types to match on - "Create" | "Update" | "Delete" - > {} +const defaultBus = EventBus.default(scope); +defaultBus.when("lambdaRule", (event) => event.source === "lambda"); ``` -## Create an EventBusRule +## Adopting CDK Resources -Now that you have a wrapped `EventBus`, lets add some rules. - -Functionless lets you write logic in Typescript on the type safe event. - -Lets match all of the `Create` or `Update` events with one rule and another rule for `Delete`s. +To turn a CDK `aws_events.EventBus` into a Functionless `EventBus` using `EventBus.fromBus`. ```ts -const createOrUpdateEvents = bus.when( - this, - "createOrUpdateRule", - (event) => - event["detail-type"] === "Create" || event["detail-type"] === "Update" -); -const deleteEvents = bus.when( - this, - "deleteRule", - (event) => event["detail-type"] === "Delete" -); +const awsBus = new aws_events.EventBus(stack, "awsBus"); +const bus = EventBus.fromBus(awsBus); ``` -We also want to do something special when we get a new cat lover who is between 18 and 30 years old, lets make another rule for those. +This can also be done with imported AWS CDK buses ```ts -const catPeopleEvents = bus.when( - (event) => - event["detail-type"] === "Create" && - event.detail.interests.includes("CATS") && - event.detail.age >= 18 && - event.detail.age < 30 +const awsBus = new aws_events.EventBus.fromEventBusName( + stack, + "awsBus", + "someBusInTheAccount" ); +const bus = EventBus.fromBus(awsBus); ``` -Rules can be further refined by calling `when` on a Functionless `EventBusRule`. - -```ts -// Cat people who are between 18 and 30 and do not also like dogs. -catPeopleEvents.when((event) => !event.detail.interests.includes("DOGS")); -``` - -## Transform the event before sending to some services like `Lambda` Functions. - -We have two lambda functions to invoke, one for create or updates and another for deletes, lets make those. - -```ts -const createOrUpdateFunction = new aws_lambda.Function(this, 'createOrUpdate', ...); -const deleteFunction = new aws_lambda.Function(this, 'delete', ...); -``` - -and wrap them with Functionless's `Function` wrapper, including given them input types. - -```ts -interface CreateOrUpdate { - id?: string; - name: string; - age: number; - operation: "Create" | "Update"; - interests: string[]; -} - -interface Delete { - id: string; -} +## Put Events to your bus from other Resources -const createOrUpdateOperation = functionless.Function( - createOrUpdateFunction -); -const deleteOperation = functionless.Function(deleteFunction); -``` +The `putEvents` integration allows other resources to easily send events to your `EventBus`. The integration is supported in [Lambda Functions](../function), [Step Functions](../step-function), and [AppSync Resolvers](../appsync). -The events from before do not match the formats from before, so lets transform them to the structures match. +For a full list, see: [Integrations](./integrations#to-eventbus) ```ts -const createOrUpdateEventsTransformed = - createOrUpdateEvents.map((event) => ({ - id: event.detail.id, - name: event.detail.name, - age: event.detail.age, - operation: event["detail-type"], - interests: event.detail.interests, - })); - -const deleteEventsTransformed = createOrUpdateEvents.map((event) => ({ - id: event.detail.id, -})); -``` - -## Target other AWS services like Lambda and other Event Buses - -Now that we have created rules on our event buses using `when` and transformed those matched events using `map`, we need to send the events somewhere. - -We can `pipe` the transformed events to the lambda functions we defined earlier. - -```ts -createOrUpdateEventsTransformed.pipe(createOrUpdateOperation); -deleteEventsTransformed.pipe(deleteOperation); -``` - -What about our young cat lovers? We want to forward those events to our sister team's event bus for processing. - -```ts -const catPeopleBus = functionless.EventBus.fromBus( - aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catTeamBusArn) -); - -// Note: EventBridge does not support transforming events which target other event buses. These events are sent as is. -catPeopleEvents.pipe(catPeopleBus); -``` - -## Put Events from other sources - -Event Bridge Put Events API is one of the methods for putting new events on an event bus. We support some first party integrations between services and event bus. - -Support (See [issues](https://github.com/functionless/functionless/issues?q=is%3Aissue+is%3Aopen+label%3Aevent-bridge) for progress): - -- Step Functions -- App Sync (coming soon) -- API Gateway (coming soon) -- More - Please create a new issue in the form `Event Bridge + [Service]` - -```ts -bus = new EventBus(stack, "bus"); +const bus = new EventBus(stack, "bus"); new StepFunction<{ value: string }, void>((input) => { - bus({ + bus.putEvents({ detail: { value: input.value, }, + source: "mySource", + "detail-type": "myEventType", }); }); ``` - -This will create a step function which sends an event. It is also possible to send multiple events and use other Step Function logic. - -> Limit: It is not currently possible to dynamically generate different numbers of events. All events sent must start from objects in the form `{ detail: ..., source: ... }` where all fields are optional. - -## Summary - -Lets look at the above all together. - -```ts -interface UserDetails { - id?: string; - name: string; - age: number; - interests: string[]; -} - -interface UserEvent - extends functionless.EventBusRuleInput< - UserDetails, - // We can provide custom detail-types to match on - "Create" | "Update" | "Delete" - > {} - -interface CreateOrUpdate { - id?: string; - name: string; - age: number; - operation: "Create" | "Update"; - interests: string[]; -} - -interface Delete { - id: string; -} - -const createOrUpdateFunction = new functionless.Function( - new aws_lambda.Function(this, "createOrUpdate", { ... }) -); - -const deleteFunction = new functionless.Function( - new aws_lambda.Function(this, "delete", { ... }) -); - -const bus = new functionless.EventBus(this, "myBus"); - -// Create and update events are sent to a specific lambda function. -bus - .when( - this, - "createOrUpdateRule", - (event) => - event["detail-type"] === "Create" || event["detail-type"] === "Update" - ) - .map((event) => ({ - id: event.detail.id, - name: event.detail.name, - age: event.detail.age, - operation: event["detail-type"] as "Create" | "Update", - interests: event.detail.interests, - })) - .pipe(createOrUpdateFunction); - -// Delete events are sent to a specific lambda function. -bus - .when(this, "deleteRule", (event) => event["detail-type"] === "Delete") - .map((event) => ({ - id: event.detail.id!, - })) - .pipe(deleteFunction); - -// New, young users interested in cat are forwarded to our sister team. -bus - .when( - this, - "catLovers", - (event) => - event["detail-type"] === "Create" && - event.detail.interests.includes("CATS") && - event.detail.age >= 18 && - event.detail.age < 30 - ) - .pipe( - functionless.EventBus.fromBus( - aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catBusArn) - ) - ); -``` - -## Adapt a CDK aws_events.EventBus - -Functionless uses a wrapped version of CDK's Event Bus, lets create a CDK event bus first. - -```ts -// Create a new Event Bus using CDK. -const bus = new functionless.EventBus(this, "myBus"); - -// Functionless also supports using the default bus or importing an Event Bus. -const awsBus = functionless.EventBus.fromBus( - new aws_events.EventBus(this, "awsBus") -); -const defaultBus = functionless.EventBus.fromBus( - aws_events.EventBus.fromEventBusName(this, "defaultBus", "default") -); -const importedBus = functionless.EventBus.fromBus( - aws_events.EventBus.fromEventBusArn(this, "defaultBus", arn) -); -``` diff --git a/website/docs/concepts/event-bridge/event-sources.md b/website/docs/concepts/event-bridge/event-sources.md new file mode 100644 index 00000000..66c535da --- /dev/null +++ b/website/docs/concepts/event-bridge/event-sources.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 98 +--- + +# Event Sources + +Some Functionless resources provide built in event sources. Event Sources are pre-configured rules which match the events output by default or configuration for various resources. + +For example, Step Functions provides an event to the `default` bus each time the status changes for a machine execution: + +```ts +const sfn = new StepFunction(...); +const successEvents = sfn.onSucceeded(stack, 'successEvent') + .map(...) // optionally, transform + .pipe(...); // and send somewhere; +``` + +Which is the same as doing: + +```ts +const sfn = new StepFunction(...); +EventBus.default(scope) + .when("sfnSuccessRule", (event) => event.detail.status === "SUCCEEDED" + && event.source === "aws.states" + && event.detail.stateMachineArn === sfn.stateMachineArn) + .map(...) + .pipe(...); +``` + +## Resources with Event Sources + +| Resource | events | +| ------------------------------------------------ | ------------------------------------------------------------ | +| [Step Functions](../step-function/event-sources) | succeeded, failed, statusChanged, aborted, started, timedOut | diff --git a/website/docs/concepts/event-bridge/index.md b/website/docs/concepts/event-bridge/index.md index 7acdbc91..6e7a3211 100644 --- a/website/docs/concepts/event-bridge/index.md +++ b/website/docs/concepts/event-bridge/index.md @@ -18,8 +18,6 @@ bus .pipe(onSignupStepFunction); ``` -To jump right into building, see the [`EventBus`](./event-bus.md) documentation. - ## What is AWS Event Bridge? [AWS Event Bridge](https://aws.amazon.com/eventbridge/) is a fully managed pub-sub service capable of ingesting an arbitrary number of events from upstream services, (optionally) filtering and transforming them, before (finally) forwarding them to downstream services. Event Bridge enables the development of more scalable systems by de-coupling the producer of an event from its consumers(s). It is a highly managed service, capable of arbitrary scale and is configured declaratively with pure JSON documents - so there is no runtime code for the developer to maintain. @@ -28,6 +26,259 @@ To jump right into building, see the [`EventBus`](./event-bus.md) documentation. An instance of an [`EventBus`](./event-bus.md) ingest events and routes them to downstream integrations according to Rules created by the user. Events are sent to downstream services such as Lambda Functions, Step Functions, or a third party (non-AWS) API. Sources of events include other AWS Resources or third party (non-AWS) SaaS products, e.g. a Slack webhook. -## Default Event Bus +## Integrations + +Functionless supports integrations between some AWS services and Event Bridge. Send events to an `EventBus` using the `putEvents` API and send events to other resources using the `.pipe` method. + +### `Pipe` events from an `EventBus`. + +```ts +new EventBus(stack, "bus") + .when("onSignUp", (event) => event.source === "lambda") + // send an event to a lambda + .pipe( + new Function(stack, "func", async (event) => { + console.log(event.id); + }) + ); +``` + +### `putEvents` to an `EventBus` + +```ts +const bus = new EventBus(stack, "bus"); +new StepFunction<{ value: string }, void>((input) => { + bus.putEvents({ + detail: { + value: input.value, + }, + source: "mySource", + "detail-type": "myEventType", + }); +}); +``` + +See [Integrations](./integrations) for more details. + +## Declare an Event Type + +Functionless supports typesafe events, [Rule](./rule.md), [Transforms](./transform.md) and [Integrations](#integrations). These types can be used to maintain type safety throughout your application, generate documentation, maintain a record of your schema in your code base, and use schemas/types provided by dependencies. + +Lets create some for this example. + +```ts +interface UserDetails { + id?: string; + name: string; + age: number; + interests: string[]; +} + +interface UserEvent + extends functionless.Event< + UserDetails, + // We can provide custom detail-types to match on + "Create" | "Update" | "Delete" + > {} +``` + +## Create or wrap an Event Bus + +To access Functionless features, create a Functionless `EventBus` or wrap a cdk `aws_events.EventBus`. + +```ts +const bus = new EventBus(stack, "bus"); +// or by adopting a aws CDK EventBus +const busFromAws = EventBus.fromBus( + new aws_events.EventBus(stack, "bus") +); +``` + +## Create a Rule + +Functionless lets you write logic in Typescript on the type safe event. + +Lets match all of the `Create` or `Update` events with one rule and another rule for `Delete`s. + +```ts +const createOrUpdateEvents = bus.when( + this, + "createOrUpdateRule", + (event) => + event["detail-type"] === "Create" || event["detail-type"] === "Update" +); +const deleteEvents = bus.when( + this, + "deleteRule", + (event) => event["detail-type"] === "Delete" +); +``` + +We also want to do something special when we get a new cat lover who is between 18 and 30 years old, lets make another rule for those. + +```ts +const catPeopleEvents = bus.when("catPeopleRule" + (event) => + event["detail-type"] === "Create" && + event.detail.interests.includes("CATS") && + event.detail.age >= 18 && + event.detail.age < 30 +); +``` + +Rules can be further refined by calling `when` on a Functionless `Rule`. + +```ts +// Cat people who are between 18 and 30 and do not also like dogs. +catPeopleEvents.when( + "catAndNotDogPeopleRule", + (event) => !event.detail.interests.includes("DOGS") +); +``` + +## Transform the event before sending to some services like `Lambda` Functions. + +We have two lambda functions to invoke, one for create or updates and another for deletes, lets make those. + +```ts +interface CreateOrUpdate { + id?: string; + name: string; + age: number; + operation: "Create" | "Update"; + interests: string[]; +} -There is a default Event Bus in every region of an AWS account. It contains events emitted by your AWS Resources, such as when a Step Function execution completes, or when a scheduled trigger fires. +interface Delete { + id: string; +} + +const createOrUpdateFunction = new Function( + this, + "createOrUpdate", + async (event: CreateOrUpdate) => { + /** implement me **/ + } +); +const deleteFunction = new Function(this, "delete", async (event: Delete) => { + /** implement me **/ +}); +``` + +The events from before do not match the formats from before, so lets transform them to the structures match. + +```ts +const createOrUpdateEventsTransformed = + createOrUpdateEvents.map((event) => ({ + id: event.detail.id, + name: event.detail.name, + age: event.detail.age, + operation: event["detail-type"], + interests: event.detail.interests, + })); + +const deleteEventsTransformed = createOrUpdateEvents.map((event) => ({ + id: event.detail.id, +})); +``` + +## Target other AWS services like Lambda and other Event Buses + +Now that we have created rules on our event buses using `when` and transformed those matched events using `map`, we need to send the events somewhere. + +We can `pipe` the transformed events to the lambda functions we defined earlier. + +```ts +createOrUpdateEventsTransformed.pipe(createOrUpdateFunction); +deleteEventsTransformed.pipe(deleteFunction); +``` + +What about our young cat lovers? We want to forward those events to our sister team's event bus for processing. + +```ts +const catPeopleBus = functionless.EventBus.fromBus( + aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catTeamBusArn) +); + +// Note: EventBridge does not support transforming events which target other event buses. These events are sent as is. +catPeopleEvents.pipe(catPeopleBus); +``` + +## Putting it all together. + +Lets look at the above all together. + +```ts +interface UserDetails { + id?: string; + name: string; + age: number; + interests: string[]; +} + +interface UserEvent + extends functionless.Event< + UserDetails, + // We can provide custom detail-types to match on + "Create" | "Update" | "Delete" + > {} + +interface CreateOrUpdate { + id?: string; + name: string; + age: number; + operation: "Create" | "Update"; + interests: string[]; +} + +interface Delete { + id: string; +} + +const createOrUpdateFunction = new functionless.Function(this, 'createOrUpdate', async (event: CreateOrUpdate) => { /** implement me **/ }); +const deleteFunction = new functionless.Function(this, 'delete', async (event: Delete) => { /** implement me **/ }); + +const bus = new functionless.EventBus(this, "myBus"); + +// Create and update events are sent to a specific lambda function. +bus + .when( + this, + "createOrUpdateRule", + (event) => + event["detail-type"] === "Create" || event["detail-type"] === "Update" + ) + .map((event) => ({ + id: event.detail.id, + name: event.detail.name, + age: event.detail.age, + operation: event["detail-type"] as "Create" | "Update", + interests: event.detail.interests, + })) + .pipe(createOrUpdateFunction); + +// Delete events are sent to a specific lambda function. +bus + .when(this, "deleteRule", (event) => event["detail-type"] === "Delete") + .map((event) => ({ + id: event.detail.id!, + })) + .pipe(deleteFunction); + +// New, young users interested in cat are forwarded to our sister team. +bus + .when( + this, + "catLovers", + (event) => + event["detail-type"] === "Create" && + event.detail.interests.includes("CATS") && + event.detail.age >= 18 && + event.detail.age < 30 + ) + .pipe( + functionless.EventBus.fromBus( + aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catBusArn) + ) + ); +``` diff --git a/website/docs/concepts/event-bridge/integrations.md b/website/docs/concepts/event-bridge/integrations.md new file mode 100644 index 00000000..2e16a2c2 --- /dev/null +++ b/website/docs/concepts/event-bridge/integrations.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 5 +--- + +# Integrations + +Functionless supports integrations between some AWS services and Event Bridge. Send events to an `EventBus` using the `putEvents` API and send events to other resources using the `.pipe` method. + +| Resource | From `EventBus` | To `EventBus` | +| -------------- | --------------- | ------------- | +| _via_ | `pipe` | `putEvents` | +| Lambda | ✅ | ✅ | +| Step Functions | ✅ | ✅ | +| EventBus | ✅ | ✅ | +| App Sync | | Coming Soon | +| API Gateway | | Coming Soon | + +See [issues](https://github.com/functionless/functionless/issues?q=is%3Aissue+is%3Aopen+label%3Aevent-bridge) for progress or create a new issue in the form `Event Bridge + [Service]`. + +## From `EventBus` using `pipe` + +```ts +new EventBus(stack, "bus") + .when("onSignUp", (event) => event.source === "lambda") + // send an event to a lambda + .pipe( + new Function(stack, "func", async (event) => { + console.log(event.id); + }) + ); +``` + +### Escape Hatches + +If a target isn't supported by Functionless, `.pipe` supports [any target supported by EventBridge](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events_targets-readme.html). + +```ts +const logGroup = new aws_logs.LogGroup(this, "MyLogGroup", { + logGroupName: "MyLogGroup", +}); + +// use the pipe callback escape hatch to pipe to a cloudwatch log group (which functionless doesn't natively support, yet) +new EventBus() + .when("rule1", (event) => event.source === "lambda") + .map((event) => `log me ${event.id}`) + .pipe( + (targetInput) => + new targets.CloudWatchLogGroup(logGroup, { event: targetInput }) + ); + +// or without Functionless's transform +new EventBus() + .when("rule2", (event) => event.source === "lambda") + .pipe(() => new targets.CloudWatchLogGroup(logGroup)); +``` + +See [issues](https://github.com/functionless/functionless/issues?q=is%3Aissue+is%3Aopen+label%3Aevent-bridge) for progress or create a new issue in the form `Event Bridge + [Service]`. + +## To `EventBus` + +### Step Functions + +```ts +const bus = new EventBus(); += new StepFunction(stack, "sfn", () => { + bus.putEvents({ + source: "myStepFunction", + "detail-type": "someType", + detail: {}, + }); +}); +``` + +:::caution +Limitation: [Events passed to the bus in a step function must one or more literal objects](./integrations#Events_passed-to_the_bus_in_a_step_function_must_literal_objects) and may not use the spread (`...`) syntax. +::: + +### Lambda + +```ts +const bus = new EventBus(); +new Lambda(stack, "sfn", async () => { + bus.putEvents({ + source: "myFunction", + "detail-type": "someType", + detail: {}, + }); +}); +``` + +### Event Bus + +Bus to bus sends events directly between two event buses. + +:::info +See AWS's documentation for limitations with [cross-account](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html) and [cross-region](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html) bus to bus events. +::: + +```ts +const eventBus = new EventBus(stack, "bus1"); +const eventBus2 = new EventBus(stack, "bus2"); +// send lambda events from bus1 to bus2. +eventBus + .when("lambdaRule", (event) => event.source === "lambda") + .pipe(eventBus2); +``` + +:::info +Event Bridge does not support transforming events when sending between buses. + +```ts +const bus = new EventBus(); +bus + .all() + .map((event) => event.id) + .pipe(bus); // fails +bus.all().pipe(bus); // works +``` + +::: diff --git a/website/docs/concepts/event-bridge/limitations.md b/website/docs/concepts/event-bridge/limitations.md new file mode 100644 index 00000000..6f09e681 --- /dev/null +++ b/website/docs/concepts/event-bridge/limitations.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 99 +--- + +# Limitations + +## Events passed to the bus in a step function must literal objects + +Events passed to the bus in a step function must be one or more literal objects and may not use the spread (`...`) syntax. + +```ts +const sfn = new StepFunction(stack, "sfn", () => { + const event = { source: "lambda", "detail-type": "type", detail: {} }; + bus.putEvents(event); // error + bus.putEvents({ ...event }); // error + bus.putEvents(...[event]); // error + bus.putEvents({ + // works + source: "lambda", + "detail-type": "type", + detail: {}, + }); +}); +``` + +### Workaround + +Lambda can be used to generate dynamic event collections. + +```ts +const sender = new Function(stack, "sender", async (event) => { + const event = { source: "lambda", "detail-type": "type", detail: {} }; + bus.putEvents(event); // valid + bus.putEvents({ ...event }); // valid + bus.putEvents(...[event]); // valid + bus.putEvents({ + // works + source: "lambda", + "detail-type": "type", + detail: {}, + }); +}); + +const sfn = new StepFunction(stack, "sfn", () => { + const event = { source: "lambda", "detail-type": "type", detail: {} }; + sender(event); +}); +``` + +The limitation is due to Step Function's lack of optional or default value retrieval for fields. Attempting to access a missing field in ASL leads to en error. This can be fixed using Choice/Conditions to check for the existence of a single field, but would take all permutations of all optional fields to support optional field at runtime. Due to this limitation, we currently compute the transformation at compile time using the fields present on the literal object. For more details and process see: https://github.com/functionless/functionless/issues/101. + +## Bus to Bus rules cannot be transformed + +Event Bridge supports forwarding events from one bus to another, including across accounts and region. + +:::info +See AWS's documentation for limitations with [cross-account](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html) and [cross-region](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html) bus to bus events. +::: + +However, unlike sending events to Lambda or StepFunctions, the inputs cannot be transformed between buses. + +```ts +const bus1 = new EventBus(stack, "bus1"); +const bus2 = new EventBus(stack, "bus2"); + +bus1 + .all() + // we want to change the source before forwarding the events. + .map((event) => ({ + source: "bus1", + detail: event.detail, + "detail-type": event["detail-type"], + })) + .pipe(bus2); // invalid - cannot follow a map with a pipe to another bus. + +// valid +bus1.all().pipe(bus2); +``` + +### Workaround + +As a workaround, Lambda can be used to transform events. + +```ts +const bus1 = new EventBus(stack, "bus1"); +const bus2 = new EventBus(stack, "bus2"); + +// the forwarder transforms the event and then sends to bus 2 for us. +const forwarder = new Function(stack, "forwarder", async (event) => { + const updatedEvent = { ...event, source: "bus1" }; + bus2.putEvent(updatedEvent); +}); + +bus1 + .all() + // we want to change the source before forwarding the events. + .pipe(forwarder); // invalid - cannot follow a map with a pipe to another bus. +``` diff --git a/website/docs/concepts/event-bridge/rule.md b/website/docs/concepts/event-bridge/rule.md index 47ca902e..afbe1f33 100644 --- a/website/docs/concepts/event-bridge/rule.md +++ b/website/docs/concepts/event-bridge/rule.md @@ -3,3 +3,127 @@ sidebar_position: 2 --- # Rule + +[Event Bus Rules](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html) filter the event on a bus and then send those events to a target with optional transformation. + +Functionless allows typescript to be used when defining a rule. + +```ts +const bus = new EventBus(stack, 'bus'); +const func = new Function(stack, 'func', (event) => console.log(event.id)); + +// an event bridge rule made with Functionless +const lambdaEventsRule = bus + // filters the events on the bus to only event from lambda (or with a source value of `lambda`). + .when('lambdaEvents', event => event.source === "lambda"); + // send all matched events to the given function. + .pipe(func); +``` + +The above is equal to the below in CDK: + +```ts +declare const func: aws_lambda.IFunction; +const bus = aws_events.EventBus(stack, "bus"); +const lambdaEventsRule = aws_events.Rule(bus, "lambdaEvents", { + eventBus: bus, + eventPattern: { source: ["lambda"] }, +}); +lambdaEventsRule.addTarget(new aws_event_targets.LambdaFunction(func)); +``` + +:::info +For more details on the supported schema for `Rule`s see [syntax](./syntax#event-patterns) +::: + +## Scheduled Rules + +Functionless supports a thin wrapper around the `EventBus` scheduled events. + +Event Bridge only supports scheduled rules on the `default` bus. + +```ts +EventBus.scheduled( + stack, + "myScheduledRule", + aws_events.Schedule.duration(Duration.hour(1)) +); +// is the same as +EventBus.default(stack).scheduled( + stack, + "myScheduledRule2", + aws_events.Schedule.duration(Duration.hour(1)) +); +// or in regular CDK: +new aws_events.Rule(stack, "myScheduledRule", { + schedule: aws_events.Schedule.duration(Duration.hour(1)), +}); +``` + +Then `map` and/or `pipe` from the scheduled rules. + +```ts +EventBus + .scheduled(stack, 'myScheduledRule', + aws_events.Schedule.duration(Duration.hour(1))) + .map(event => event.id) + .pipe(new Function(...)); +``` + +## Match All Rule + +To create a rule that matches all events on a bus, use the `.all` helper on the `EventBus` + +```ts +const bus = new EventBus(stack, "bus"); +const allEvents = bus.all(); +// or +const allEventWhen = bus.when("allBusEvent", () => true); +``` + +:::info +By default the `.all()` overload uses a singleton rule with the name `"all"` and scope `EventBus`. To create a unique `.all` rule or put the rule on another `Stack`, use the `.all(scope, id)` overload; + +```ts +declare const bus: EventBus; +const allEvents = bus.all(anotherStackOrConstruct, "newAllRule"); +``` + +::: + +## Refining Rules + +`Rule`s can be refined using the `.when` on the `Rule` object. Chained `.when` statement act like AND logic between the new and previous predicates. + +```ts +const bus = new EventBus(stack, "bus"); + +// an event bridge rule made with Functionless +const lambdaEventsRule = bus + // filters the events on the bus to only event from lambda (or with a source value of `lambda`). + .when("lambdaEvents", (event) => event.source === "lambda"); + +// all lambda events with the detail type "some type" +lambdaEventsRule.when("rule1", (event) => event["detail-type"] === "some type"); +``` + +## Escape Hatches + +If the rule behavior desired isn't supported by Functionless, Functionless can wrap any valid CDK `aws_events.Rule`. + +```ts +interface MyEvent extends Event<{}, "specialEvent", "mySource"> {} + +const wrappedRule = Rule.fromRule( + new aws_events.Rule(stack, "rule", { + eventBus: aws_events.EventBus.fromEventBusName(stack, "myBus", "someBus"), + eventPattern: { + source: ["lambda"], + }, + }) +); + +wrappedRule.pipe(new Function(stack, "func", (event) => console.log(event.id))); +``` + +Check [issues](https://github.com/functionless/functionless/issues?q=is%3Aissue+is%3Aopen+label%3Aevent-bridge) to see if your use case is known or create a new issue in the form `Event Bridge + [Use Case|Bug]`. diff --git a/website/docs/concepts/event-bridge/syntax.md b/website/docs/concepts/event-bridge/syntax.md index 6160e300..bad59013 100644 --- a/website/docs/concepts/event-bridge/syntax.md +++ b/website/docs/concepts/event-bridge/syntax.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 99 --- # Syntax @@ -11,13 +11,13 @@ Event patterns are all predicates that filter on the incoming event. The pattern https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ```ts -.when(event => event.detail.value === "something") +.when("rule", event => event.detail.value === "something") ``` ### Equals ```ts -.when(event => event.source === "lambda") +.when("rule", event => event.source === "lambda") ``` ```json @@ -29,7 +29,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ### Not Equals ```ts -.when(event => event.source !== "lambda") +.when("rule", event => event.source !== "lambda") ``` ```json @@ -41,7 +41,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ### Starts With ```ts -.when(event => event.source.startsWith("lambda")) +.when("rule", event => event.source.startsWith("lambda")) ``` ```json @@ -53,7 +53,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ### Not Starts With ```ts -.when(event => !event.source.startsWith("lambda")) +.when("rule", event => !event.source.startsWith("lambda")) ``` ```json @@ -67,7 +67,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ### List Includes ```ts -.when(event => event.resources.includes("some arn")) +.when("rule", event => event.resources.includes("some arn")) ``` ```json @@ -81,7 +81,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html ### Numbers ```ts -.when(event => event.detail.age > 30 && event.detail.age <= 60) +.when("rule", event => event.detail.age > 30 && event.detail.age <= 60) ``` ```json @@ -95,7 +95,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html Non-converging ranges ```ts -.when(event => event.detail.age < 30 || event.detail.age >= 60) +.when("rule", event => event.detail.age < 30 || event.detail.age >= 60) ``` ```json @@ -109,7 +109,7 @@ Non-converging ranges Inversion ```ts -.when(event => !(event.detail.age < 30 && event.detail.age >= 60)) +.when("rule", event => !(event.detail.age < 30 && event.detail.age >= 60)) ``` ```json @@ -123,7 +123,7 @@ Inversion Reduction ```ts -.when(event => (event.detail.age < 30 || event.detail.age >= 60) && +.when("rule", event => (event.detail.age < 30 || event.detail.age >= 60) && (event.detail.age < 20 || event.detail.age >= 50) && event.detail.age > 0) ``` @@ -141,7 +141,7 @@ Reduction > Limit: Event Bridge patterns do not support OR logic between fields. The logic `event.source === "lambda" || event['detail-type'] === "LambdaLike"` is impossible within the same rule. ```ts -.when(event => event.source === "lambda" || event.source === "dynamo") +.when("rule", event => event.source === "lambda" || event.source === "dynamo") ``` ```json @@ -155,7 +155,7 @@ Reduction > Limit: Except for the case of numeric ranges and a few others Event Bridge does not support AND logic within the same field. The logic `event.resources.includes("resource1") && event.resources.includes("resource2")` is impossible. ```ts -.when(event => event.source === "lambda" && event.id.startsWith("idPrefix")) +.when("rule", event => event.source === "lambda" && event.id.startsWith("idPrefix")) ``` ```json @@ -170,8 +170,8 @@ Reduction Exists ```ts -.when(event => event.detail.optional !== undefined) -.when(event => !!event.detail.optional) +.when("rule", event => event.detail.optional !== undefined) +.when("rule", event => !!event.detail.optional) ``` ```json @@ -185,8 +185,8 @@ Exists Does not Exist ```ts -.when(event => event.detail.optional === undefined) -.when(event => !event.detail.optional) +.when("rule", event => event.detail.optional === undefined) +.when("rule", event => !event.detail.optional) ``` ```json @@ -200,7 +200,7 @@ Does not Exist Simplification ```ts -.when(event => event.detail.optional && event.detail.optional === "value") +.when("rule", event => event.detail.optional && event.detail.optional === "value") ``` ```json @@ -222,7 +222,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-inp ## Constant ```ts -.map(() => "got one!") +.map("rule", () => "got one!") ``` ```json @@ -234,7 +234,7 @@ https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-inp ## String field ```ts -.map(event => event.source) +.map("rule", event => event.source) ``` Simple inputs can use `eventPath`. @@ -248,7 +248,7 @@ Simple inputs can use `eventPath`. ## Formatted String ```ts -.map(event => `the source is ${event.source}`) +.map("rule", event => `the source is ${event.source}`) ``` ```json @@ -263,7 +263,7 @@ Simple inputs can use `eventPath`. ## Whole Event ```ts -.map(event => event) +.map("rule", event => event) ``` ```json @@ -276,7 +276,7 @@ Simple inputs can use `eventPath`. ## Rule Name and Rule Arn ```ts -.map((event, $utils) => `name: ${$utils.context.ruleName} arn: ${$utils.context.ruleArn}`) +.map("rule", (event, $utils) => `name: ${$utils.context.ruleName} arn: ${$utils.context.ruleArn}`) ``` ```json @@ -289,7 +289,7 @@ Simple inputs can use `eventPath`. ## Constant Objects ```ts -.map(event => event.detail) +.map("rule", event => event.detail) ``` ```json @@ -301,7 +301,7 @@ Simple inputs can use `eventPath`. ## Objects ```ts -.map(event => ({ +.map("rule", event => ({ value: event.detail.field, source: event.source, constant: "hello" diff --git a/website/docs/concepts/event-bridge/transform.md b/website/docs/concepts/event-bridge/transform.md new file mode 100644 index 00000000..8c001026 --- /dev/null +++ b/website/docs/concepts/event-bridge/transform.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 3 +--- + +# Transform + +[Event Bus Input Transforms](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html) transform events matched by [Rules](./rule). Transforms supports a loosely coupled application by allowing the rule to normalize an event before sending it to a target with it's own expected payload schema. + +Functionless allows Typescript to be used when defining an input transform. It transforms the code given into Event Bridge's [Input Transform](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html) JSON schema, while maintaining the contract provided by the [Typescript Syntax](./syntax#event-transforms). + +```ts +const bus = new EventBus(stack, 'bus'); +const func = new Function(stack, 'func', (input) => console.log(input)); + +// an event bridge rule made with Functionless +const lambdaEventsRule = bus + // For all events + .all(); + // send only the `id` string to the target. + .map(event => event.id) + // send all matched events to the given function. + .pipe(func); +``` + +The above is equal to the below in CDK: + +```ts +declare const func: aws_lambda.IFunction; +const bus = aws_events.EventBus(stack, "bus"); +const lambdaEventsRule = aws_events.Rule(bus, "lambdaEvents", { + eventBus: bus, + // matches all events. equivalent to .all() or .when(event => true) + eventPattern: { source: [{ prefix: "" }] }, +}); +lambdaEventsRule.addTarget( + new aws_event_targets.LambdaFunction(func, { + // equivalent to .map(event => event.id) + event: aws_events.RuleTargetInput.fromEventPath("$.id"), + }) +); +``` + +:::info +Where does the `{ source: [{ prefix: "" }] }` syntax come from? + +Rules have no explicit way to send all events, this hack was proposed on [StackOverflow](https://stackoverflow.com/a/62407802/968011). + +Why does this work? + +- The `source` field is always required and is always a string. +- `{ prefix: "" }` ([Prefix Matching](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-prefix-matching)) will match any string. + ::: + +:::info +For more details on the supported schema for `Transform`s see [syntax](./syntax#event-transforms) +::: + +:::caution +[Bus to bus targets (`.pipe`) cannot be transformed](./limitations#bus-to-bus-rules-cannot-be-transformed). +::: + +## Utilities + +EventBridge provides [predefined variables](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html#eb-transform-input-predefined) to inject additional information when transforming events. + +Most of the predefined variables can be accessed using the second parameter to the `.map` function's callback. + +```ts +const bus = new EventBus(stack, "bus") + .all() + // send the rule name to a lambda for each event on the bus + .map((event, $utils) => $utils.context.ruleName) + .pipe( + new Function(stack, "func", (ruleName) => + console.log(`rule name: ${ruleName}`) + ) + ); +``` + +| Variable | Functionless | Description | +| -------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Rule Arn | `(event, $utils) => $utils.context.ruleName` | The Amazon Resource Name (ARN) of the EventBridge rule. | +| Rule Name | `(event, $utils) => $utils.context.ruleName` | The Name of the EventBridge rule. | +| Ingestion Time | `(event, $utils) => $utils.context.ingestionTime` | The time at which the event was received by EventBridge. This variable is generated by EventBridge and can't be overwritten. | +| Event Json | `(event, $utils) => $utils.context.eventJson` | The exact payload of an event as a string. | +| Event | `(event) => event` | A copy of the original event. | + +### Whole Event + +To make use of the whole event predefined variable `aws.events.event`, use the event object when transforming. + +```ts +const bus = new EventBus(stack, "bus") + .all() + // send the whole event to a lambda + .map((event, $utils) => ({ + eventId: event.id, + // uses the `aws.events.event` predefined variable to send to whole event. + event: event, + })) + .pipe( + new Function(stack, "func", ({ eventId, event }) => + console.log(`event id: ${eventId}`, event) + ) + ); +``` diff --git a/website/docs/concepts/step-function/event-sources.md b/website/docs/concepts/step-function/event-sources.md new file mode 100644 index 00000000..f02bd5a7 --- /dev/null +++ b/website/docs/concepts/step-function/event-sources.md @@ -0,0 +1,52 @@ +# Event Sources + +AWS Step Functions sends [Event Bus events](https://docs.aws.amazon.com/step-functions/latest/dg/cw-events.html) for each machine execution. Functionless provides easy access to them through Event Bus [Event Sources](../event-bridge/event-sources). + +```ts +const succeededExecutions = + new StepFunction(stack, 'sfn', () => ...) + .onSucceeded(stack, 'succeeded'); +succeededExecutions.pipe(new Function(...)); +``` + +## Event Sources + +| Event | Method | STATUS | Description | +| ------------- | ------------------- | --------- | ---------------------------- | +| Succeeded | `onSucceeded()` | SUCEEDEED | When an execution succeeds. | +| Failed | `onFailed()` | FAILED | When an execution fails. | +| Aborted | `onAborted()` | ABORTED | When an execution aborts. | +| TimeOut | `onTimedOut()` | TIMED_OUT | When an execution times out. | +| Started | `onStarted()` | RUNNING | When an execution starts | +| StatusChanged | `onStatusChanged()` | \ | When the status changes. | + +## Example Event + +```json +{ + "version": "0", + "id": "315c1398-40ff-a850-213b-158f73e60175", + "detail-type": "Step Functions Execution Status Change", + "source": "aws.states", + "account": "012345678912", + "time": "2019-02-26T19:42:21Z", + "region": "us-east-1", + "resources": [ + "arn:aws:states:us-east-1:012345678912:execution:state-machine-name:execution-name" + ], + "detail": { + "executionArn": "arn:aws:states:us-east-1:012345678912:execution:state-machine-name:execution-name", + "stateMachineArn": "arn:aws:states:us-east-1:012345678912:stateMachine:state-machine", + "name": "execution-name", + "status": "RUNNING", + "startDate": 1551225271984, + "stopDate": null, + "input": "{}", + "inputDetails": { + "included": true + }, + "output": null, + "outputDetails": null + } +} +```