Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,596 changes: 1,596 additions & 0 deletions event-routing-architecture-plan (2).md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions packages/ev-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@webiny/ev-test",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"@webiny/di": "^0.2.3",
"@webiny/event-handler-aws": "0.1.0",
"@webiny/event-handler-core": "0.1.0",
"@webiny/event-handler-node": "0.1.0"
}
}
35 changes: 35 additions & 0 deletions packages/ev-test/src/context/TenantContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Abstraction } from "@webiny/di";

export interface ITenant {
id: string;
}

export interface ITenantContext {
get(): ITenant | undefined;
set(tenant: ITenant): void;
require(): ITenant;
}

export const TenantContext = new Abstraction<ITenantContext>("TenantContext");

class TenantContextImpl implements ITenantContext {
private tenant?: ITenant;

get() {
return this.tenant;
}

set(tenant: ITenant) {
this.tenant = tenant;
}

require() {
if (!this.tenant) throw new Error("Tenant not set");
return this.tenant;
}
}

export const tenantContext = TenantContext.createImplementation({
implementation: TenantContextImpl,
dependencies: []
});
24 changes: 24 additions & 0 deletions packages/ev-test/src/handlers/EchoHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HttpRoute } from "@webiny/event-handler-core";
import type { IHttpRequest, IHttpResponse } from "@webiny/event-handler-core";
import { TenantContext } from "../context/TenantContext.js";
import type { ITenantContext } from "../context/TenantContext.js";

class EchoHandlerImpl implements HttpRoute.Interface {
readonly method = "POST";
readonly path = "/echo";

constructor(private tenantCtx: ITenantContext) {}

async handle(request: IHttpRequest): Promise<IHttpResponse> {
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: { tenant: this.tenantCtx.require().id, echo: request.body }
};
}
}

export const echoHandler = HttpRoute.createImplementation({
implementation: EchoHandlerImpl,
dependencies: [TenantContext]
});
41 changes: 41 additions & 0 deletions packages/ev-test/src/handlers/FilesHandler.aws.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* AWS example: serve files from S3.
*
* The handler just returns a Buffer — ApiGatewayAdapter detects it
* and sets isBase64Encoded: true automatically. No AWS-specific
* logic leaks into the handler.
*/

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { HttpRoute } from "@webiny/event-handler-core";
import type { NextFunction } from "@webiny/event-handler-core";

const s3 = new S3Client({});
const BUCKET = "my-bucket";

class FilesHandlerImpl implements HttpRoute.Interface {
private matches(event: any): boolean {
return event?.method === "GET" && event?.path?.startsWith("/files");
}

async execute(event: any, next: NextFunction) {
if (!this.matches(event)) {
return next();
}

const key = (event.path as string).replace("/files/", "");
const response = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
const body = Buffer.from(await response.Body!.transformToByteArray());

return {
statusCode: 200,
headers: { "Content-Type": response.ContentType || "application/octet-stream" },
body // Buffer → adapter sets isBase64Encoded: true
};
}
}

export const filesHandler = CloudHandler.createImplementation({
implementation: FilesHandlerImpl,
dependencies: []
});
25 changes: 25 additions & 0 deletions packages/ev-test/src/handlers/FilesHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HttpRoute } from "@webiny/event-handler-core";
import type { IHttpRequest, IHttpResponse } from "@webiny/event-handler-core";

const SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="2" fill="coral" />
<text x="50" y="55" text-anchor="middle" font-size="14" fill="white">cloudi</text>
</svg>`;

class FilesHandlerImpl implements HttpRoute.Interface {
readonly method = "GET";
readonly path = "/files/*";

async handle(_request: IHttpRequest): Promise<IHttpResponse> {
return {
statusCode: 200,
headers: { "Content-Type": "image/svg+xml" },
body: SVG
};
}
}

export const filesHandler = HttpRoute.createImplementation({
implementation: FilesHandlerImpl,
dependencies: []
});
25 changes: 25 additions & 0 deletions packages/ev-test/src/handlers/HelloHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HttpRoute } from "@webiny/event-handler-core";
import type { IHttpRequest, IHttpResponse } from "@webiny/event-handler-core";
import { GreetService } from "../services/GreetService.js";
import type { IGreetService } from "../services/GreetService.js";

class HelloHandlerImpl implements HttpRoute.Interface {
readonly method = "GET";
readonly path = "/hello";

constructor(private svc: IGreetService) {}

async handle(request: IHttpRequest): Promise<IHttpResponse> {
const name = request.query["name"] || "world";
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: { message: this.svc.greet(name) }
};
}
}

export const helloHandler = HttpRoute.createImplementation({
implementation: HelloHandlerImpl,
dependencies: [GreetService]
});
31 changes: 31 additions & 0 deletions packages/ev-test/src/handlers/TenantInitializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HttpEventHandler } from "@webiny/event-handler-core";
import type { EventContext, NextFunction } from "@webiny/event-handler-core";
import { TenantContext } from "../context/TenantContext.js";
import type { ITenantContext } from "../context/TenantContext.js";

class TenantInitializerImpl implements HttpEventHandler.Interface {
constructor(private ctx: ITenantContext) {}

async execute(ctx: EventContext, next: NextFunction) {
if (!ctx.event?.headers) {
return next();
}

const tenantId = ctx.event.headers["x-tenant"];
if (!tenantId) {
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: { error: "Missing x-tenant header" }
};
}

this.ctx.set({ id: tenantId });
return next();
}
}

export const tenantInitializer = HttpEventHandler.createImplementation({
implementation: TenantInitializerImpl,
dependencies: [TenantContext]
});
45 changes: 45 additions & 0 deletions packages/ev-test/src/index.aws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
ErrorHandler,
NotFoundHandler,
HttpRouterHandler,
HttpFeature
} from "@webiny/event-handler-core";
import {
createLambdaHandler,
ApiGatewayEventType,
ApiGatewayTranslator
} from "@webiny/event-handler-aws";
import { tenantContext } from "./context/TenantContext.js";
import { tenantInitializer } from "./handlers/TenantInitializer.js";
import { helloHandler } from "./handlers/HelloHandler.js";
import { echoHandler } from "./handlers/EchoHandler.js";
import { filesHandler } from "./handlers/FilesHandler.aws.example.js";
import { greetService } from "./services/GreetService.js";

export const handler = createLambdaHandler({
root: container => {
// Event type detection
container.register(ApiGatewayEventType);

// HTTP infrastructure
HttpFeature.register(container);

// Routes
container.register(helloHandler);
container.register(echoHandler);
container.register(filesHandler);

// Services
container.register(greetService);

// HttpEventHandler chain (runs for API Gateway events)
container.register(ApiGatewayTranslator); // first — translates APIGatewayProxyEvent → IHttpRequest
container.register(ErrorHandler);
container.register(tenantInitializer);
container.register(HttpRouterHandler);
container.register(NotFoundHandler);
},
request: container => {
container.register(tenantContext).inSingletonScope();
}
});
51 changes: 51 additions & 0 deletions packages/ev-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
ErrorHandler,
NotFoundHandler,
HttpRouterHandler,
HttpFeature
} from "@webiny/event-handler-core";
import { createNodeServer } from "@webiny/event-handler-node";
import { NodeHttpEventType, NodeHttpTranslator } from "@webiny/event-handler-node";
import { tenantContext } from "./context/TenantContext.js";
import { tenantInitializer } from "./handlers/TenantInitializer.js";
import { helloHandler } from "./handlers/HelloHandler.js";
import { echoHandler } from "./handlers/EchoHandler.js";
import { filesHandler } from "./handlers/FilesHandler.js";
import { greetService } from "./services/GreetService.js";

const server = createNodeServer({
root: container => {
// Event type detection
container.register(NodeHttpEventType);

// HTTP infrastructure (HttpRouter + SecureHeadersDecorator)
HttpFeature.register(container);

// Routes (resolved by HttpRouter)
container.register(helloHandler);
container.register(echoHandler);
container.register(filesHandler);

// Services
container.register(greetService);

// HttpEventHandler chain (runs for Node HTTP events)
container.register(NodeHttpTranslator); // first — translates IncomingMessage → IHttpRequest
container.register(ErrorHandler);
container.register(tenantInitializer);
container.register(HttpRouterHandler);
container.register(NotFoundHandler);
},
request: container => {
container.register(tenantContext).inSingletonScope();
}
});

const PORT = 3000;
server.listen(PORT, () => {
console.log(`ev-test server running on http://localhost:${PORT}`);
console.log(` GET http://localhost:${PORT}/hello?name=Adrian -H "x-tenant: acme"`);
console.log(` POST http://localhost:${PORT}/echo -H "x-tenant: acme"`);
console.log(` GET http://localhost:${PORT}/files/logo.svg -H "x-tenant: acme"`);
console.log(` GET http://localhost:${PORT}/unknown → 404`);
});
23 changes: 23 additions & 0 deletions packages/ev-test/src/services/GreetService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Abstraction } from "@webiny/di";
import { TenantContext } from "../context/TenantContext.js";
import type { ITenantContext } from "../context/TenantContext.js";

export interface IGreetService {
greet(name: string): string;
}

export const GreetService = new Abstraction<IGreetService>("GreetService");

class GreetServiceImpl implements IGreetService {
constructor(private tenantCtx: ITenantContext) {}

greet(name: string) {
const tenant = this.tenantCtx.require();
return `Hello, ${name}! (tenant: ${tenant.id})`;
}
}

export const greetService = GreetService.createImplementation({
implementation: GreetServiceImpl,
dependencies: [TenantContext]
});
Loading