Skip to content

Commit

Permalink
feat: add support for typed APIs with zod (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
sam authored Feb 10, 2023
1 parent 58db952 commit 5b54ed3
Show file tree
Hide file tree
Showing 79 changed files with 7,877 additions and 11,796 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,22 @@
"cwd": "${workspaceFolder}/apps/test-app/",
"outFiles": ["${workspaceFolder}/**/*.js", "!**/node_modules/**"]
}
{
"type": "node",
"name": "synth apps/tests/aws-runtime-cdk",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register/transpile-only",
"--enable-source-maps"
],
"args": ["src/app.ts"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}/apps/tests/aws-runtime-cdk",
"outFiles": ["${workspaceFolder}/**/*.js", "!**/node_modules/**"]
}
]
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ With our plug-and-play foundation blocks, you can use as much or as little as ne
Easily create scalable, event-driven APIs with code-first routes.

```ts
import { api, ApiResponse } from "@eventual/core";
import { api, HttpResponse } from "@eventual/core";

api.post("/echo", async (request) => {
return new ApiResponse(await request.text());
return new HttpResponse(await request.text());
});
```

Expand All @@ -87,7 +87,7 @@ interface MyEvent {

export const myEvent = event<MyEvent>("MyEvent");

myEvent.onEvent((e) => {
myEvent.onEvent("onMyEvent", (e) => {
console.log(e.key);
});
```
Expand Down
4 changes: 2 additions & 2 deletions apps/test-app-runtime/src/bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AwsHttpServiceClient } from "@eventual/aws-client";
import { AWSHttpEventualClient } from "@eventual/aws-client";

const workflowClient = new AwsHttpServiceClient({
const workflowClient = new AWSHttpEventualClient({
serviceUrl: process.env.EVENTUAL_SERVICE_URL ?? "",
});

Expand Down
6 changes: 3 additions & 3 deletions apps/test-app-runtime/src/open-account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { activity, api, ApiResponse, event, workflow } from "@eventual/core";
import { activity, api, event, HttpResponse, workflow } from "@eventual/core";

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
Expand Down Expand Up @@ -77,7 +77,7 @@ api.post("/open-account", async (request) => {
input,
});

return new ApiResponse(JSON.stringify(response), {
return new HttpResponse(JSON.stringify(response), {
headers: {
"Content-Type": "application/json",
},
Expand All @@ -87,7 +87,7 @@ api.post("/open-account", async (request) => {

const openAccountEvent = event<OpenAccountRequest>("OpenAccount");

openAccountEvent.onEvent(async (event) => {
openAccountEvent.onEvent("onOpenAccountEvent", async (event) => {
await openAccount.startExecution({
input: event,
});
Expand Down
4 changes: 2 additions & 2 deletions apps/test-app-sst/services/functions/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { event, activity, workflow, api, ApiResponse } from "@eventual/core";
import { event, activity, workflow, api, HttpResponse } from "@eventual/core";

api.post("/work", async (request) => {
const items: string[] = await request.json();
Expand All @@ -7,7 +7,7 @@ api.post("/work", async (request) => {
input: items,
});

return new ApiResponse(JSON.stringify({ executionId }), {
return new HttpResponse(JSON.stringify({ executionId }), {
status: 200,
});
});
Expand Down
4 changes: 1 addition & 3 deletions apps/test-app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ const stack = new Stack(app, "test-eventual");
const benchService = new eventual.Service(stack, "Benchmark", {
entry: require.resolve("test-app-runtime/lib/time-benchmark.js"),
workflows: {
orchestrator: {
reservedConcurrentExecutions: 100,
},
reservedConcurrentExecutions: 100,
},
});

Expand Down
4 changes: 3 additions & 1 deletion apps/test-app/src/slack-bot.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { App, CfnOutput, Stack, aws_secretsmanager } from "aws-cdk-lib";
import * as eventual from "@eventual/aws-cdk";

import type * as slackbot from "test-app-runtime/lib/slack-bot.js";

const app = new App();

const stack = new Stack(app, "slack-service");

const slackSecrets = new aws_secretsmanager.Secret(stack, "SlackSecrets");

const slackBot = new eventual.Service(stack, "slack-bot", {
const slackBot = new eventual.Service<typeof slackbot>(stack, "slack-bot", {
name: "slack-bot",
entry: require.resolve("test-app-runtime/lib/slack-bot.js"),
environment: {
Expand Down
36 changes: 22 additions & 14 deletions apps/tests/aws-runtime-cdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Queue } from "aws-cdk-lib/aws-sqs";
import path from "path";
import { ChaosExtension } from "./chaos-extension";

import type * as testServiceRuntime from "tests-runtime";

const app = new App();

const stack = new Stack(app, "eventual-tests");
Expand All @@ -25,16 +27,21 @@ const role = new Role(stack, "testRole", {

const testQueue = new Queue(stack, "testQueue");

const testService = new eventual.Service(stack, "testService", {
name: "eventual-tests",
entry: require.resolve("tests-runtime"),
environment: {
TEST_QUEUE_URL: testQueue.queueUrl,
},
logging: {
logLevel: LogLevel.DEBUG,
},
});
const testService = new eventual.Service<typeof testServiceRuntime>(
stack,
"testService",
{
name: "eventual-tests",
entry: require.resolve("tests-runtime"),
environment: {
TEST_QUEUE_URL: testQueue.queueUrl,
},
logging: {
logLevel: LogLevel.DEBUG,
},
commands: {},
}
);

testService.api.grantInvokeHttpServiceApi(role);
testService.cliRole.grantAssumeRole(role);
Expand Down Expand Up @@ -69,11 +76,12 @@ chaosExtension.grantReadWrite(role);
* Async lambda test.
*/

const entry = path.join(
require.resolve("tests-runtime"),
"../async-writer-handler.js"
);
const asyncWriterFunction = new NodejsFunction(stack, "asyncWriterFunction", {
entry: path.join(
require.resolve("tests-runtime"),
"../async-writer-handler.js"
),
entry,
handler: "handle",
environment: {
TEST_SERVICE_URL: testService.api.gateway.apiEndpoint,
Expand Down
4 changes: 2 additions & 2 deletions apps/tests/aws-runtime/test/async-writer-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Handler } from "aws-lambda";
import { AwsHttpServiceClient } from "@eventual/aws-client";
import { AWSHttpEventualClient } from "@eventual/aws-client";

const serviceClient = new AwsHttpServiceClient({
const serviceClient = new AWSHttpEventualClient({
serviceUrl: process.env.TEST_SERVICE_URL ?? "",
});

Expand Down
4 changes: 2 additions & 2 deletions apps/tests/aws-runtime/test/runtime-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
WorkflowOutput,
ExecutionHandle,
} from "@eventual/core";
import { AwsHttpServiceClient } from "@eventual/aws-client";
import { AWSHttpEventualClient } from "@eventual/aws-client";
import { chaosSSMParamName, serviceUrl } from "./env.js";
import { ChaosRule } from "./chaos-extension/chaos-engine.js";
import { SSMChaosClient } from "./chaos-extension/chaos-client.js";
import { SSMClient } from "@aws-sdk/client-ssm";

const serviceClient = new AwsHttpServiceClient({
const serviceClient = new AWSHttpEventualClient({
serviceUrl: serviceUrl(),
region: "us-east-1",
});
Expand Down
55 changes: 48 additions & 7 deletions apps/tests/aws-runtime/test/test-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
EventualError,
signal,
duration,
api,
ApiResponse,
command,
} from "@eventual/core";
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import { AsyncWriterTestEvent } from "./async-writer-handler.js";
Expand Down Expand Up @@ -322,6 +321,7 @@ const SignalEventPayload = z.object({
const signalEvent = event("SignalEvent", SignalEventPayload);

export const onSignalEvent = signalEvent.onEvent(
"onSignalEvent",
async ({ executionId, signalId, proxy }) => {
console.debug("received signal event", { executionId, signalId, proxy });
if (proxy) {
Expand Down Expand Up @@ -395,7 +395,7 @@ export const timedWorkflow = workflow("timedWorkflow", async () => {
const resumeSignal = signal("resume");
const notifyEvent = event<{ executionId: string }>("notify");

notifyEvent.onEvent(async ({ executionId }) => {
notifyEvent.onEvent("onNotifyEvent", async ({ executionId }) => {
await resumeSignal.sendSignal(executionId);
});

Expand Down Expand Up @@ -428,12 +428,53 @@ export const allCommands = workflow("allCommands", async (_, context) => {
return { signalCount: n };
});

export const userApi = api.get(
"/hello",
export const helloApi = command(
"helloApi",
{
memorySize: 512,
path: "/hello",
},
async () => {
return new ApiResponse("hello world");
return "hello world";
}
);

// provide a schema for the parameters
export const typed1 = command(
"typed1",
{
path: "/user/typed1/:userId",
input: z.object({
userId: z.string(),
}),
},
async ({ userId }) => {
return {
userId: userId,
createdTime: new Date(0).toISOString(),
};
}
);

const User = z.object({
userId: z.string(),
createdTime: z.date(),
});

// provide a schema for the output body
export const typed2 = command(
"typed2",
{
path: "/user/typed2/:userId",
method: "GET",
input: z.object({
userId: z.string(),
}),
output: User,
},
async (request) => {
return {
userId: request.userId,
createdTime: new Date(0),
};
}
);
56 changes: 54 additions & 2 deletions apps/tests/aws-runtime/test/tester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,59 @@ eventualRuntimeTestHarness(
const url = serviceUrl();

test("hello API should route and return OK response", async () => {
const response = await (await fetch(`${url}/hello`)).text();
const restResponse = await (await fetch(`${url}/hello`)).json();
const rpcResponse = await (
await fetch(`${url}/_rpc/helloApi`, {
method: "POST",
})
).json();

expect(restResponse).toEqual("hello world");
expect(rpcResponse).toEqual("hello world");
});

test("params with schema should parse", async () => {
const restResponse = await (
await fetch(`${url}/user/typed1/my-user-id`)
).json();

const rpcResponse = await (
await fetch(`${url}/_rpc/typed1`, {
method: "POST",
body: JSON.stringify({
userId: "my-user-id",
}),
})
).json();

const expectedResponse = {
userId: "my-user-id",
createdTime: new Date(0).toISOString(),
};

expect(restResponse).toEqual(expectedResponse);
expect(rpcResponse).toEqual(expectedResponse);
});

expect(response).toEqual("hello world");
test("output with schema should serialize", async () => {
const restResponse = await (
await fetch(`${url}/user/typed2/my-user-id`)
).json();

const rpcResponse = await (
await fetch(`${url}/_rpc/typed2`, {
method: "POST",
body: JSON.stringify({
userId: "my-user-id",
}),
})
).json();

const expected = {
userId: "my-user-id",
createdTime: new Date(0).toISOString(),
};

expect(restResponse).toEqual(expected);
expect(rpcResponse).toEqual(expected);
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"lint": "eslint . --fix",
"prepare": "husky install",
"test": "NODE_OPTIONS=--experimental-vm-modules turbo run test",
"test:runtime": "pnpm --filter tests-runtime test:runtime",
"test:runtime": "pnpm --filter tests-runtime test:runtime-deploy",
"test:cli": "pnpm --filter tests-runtime test:cli",
"test:smoke": "./scripts/smoke-test",
"typecheck": "tsc -b",
Expand Down
4 changes: 2 additions & 2 deletions packages/@eventual/aws-cdk/src/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface ActivitiesProps {
workflows: IWorkflows;
scheduler: IScheduler;
environment?: Record<string, string>;
events: Events;
events: Events<any>;
logging: Logging;
service: IService;
readonly api: IServiceApi;
Expand Down Expand Up @@ -101,7 +101,7 @@ export class Activities
});

this.worker = new ServiceFunction(this, "Worker", {
code: props.build.getCode(props.build.activities.default.file),
code: props.build.getCode(props.build.activities.file),
functionName: `${props.serviceName}-activity-handler`,
serviceType: ServiceType.ActivityWorker,
memorySize: 512,
Expand Down
Loading

0 comments on commit 5b54ed3

Please sign in to comment.