Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add support for typed APIs with zod #264

Merged
merged 70 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
30117fc
feat: infer memorySize, timeout, fileName and exportName from api rou…
Jan 28, 2023
94afd0a
compile manifest and drive whole CDK off of it
Jan 29, 2023
1a72ba9
fix bug with internal routes
Jan 29, 2023
35a4585
feat: carrry through the exportName as the function name
Jan 29, 2023
1907dc3
pass through memory and timeout
Jan 29, 2023
da52f8f
fix: make test independent of environment
Jan 29, 2023
e8121d2
feedback
Jan 30, 2023
396aa1a
Merge branch 'main' into sam/bundling
Jan 30, 2023
73fe165
Merge branch 'main' into sam/bundling
Jan 30, 2023
8f72ab5
add runtime tests for api
Jan 31, 2023
909cdad
update add environment to add to all handlers
Jan 31, 2023
c897f25
enable esm tests for compiler and update snapshot
Jan 31, 2023
8df08cb
set experimental-vm-modules for runtime tests
Jan 31, 2023
0396a1c
Merge branch 'main' into sam/bundling
Jan 31, 2023
7cd69e0
Merge branch 'main' into sam/bundling
Jan 31, 2023
66367b6
Merge branch 'main' into sam/bundling
Jan 31, 2023
54d2ec4
fix: add ApiResponse object and handle all body types
Jan 31, 2023
9994549
Merge branch 'main' into sam/bundling
Jan 31, 2023
2f66ee7
back to mjs
Jan 31, 2023
ce19728
Merge branch 'main' into sam/bundling
Jan 31, 2023
a3976df
remove dependency on fetch types and dom lib
Feb 1, 2023
3400f61
feat: support bunding Event Handlers individually
Feb 1, 2023
e3ad4cb
Merge branch 'main' into sam/more-bundling
Feb 1, 2023
20df9ee
export onApproval in stock-bot
Feb 1, 2023
0ce3377
feat: support Zod Schemas on event declarations for validation and Sc…
Feb 1, 2023
87d308e
give signalEvent a schema
Feb 1, 2023
9e29e84
fix lock file
Feb 1, 2023
aa64623
feat: add support for typed APIs with zod
Feb 1, 2023
204409a
Merge branch 'main' into sam/more-bundling
Feb 2, 2023
0782b14
chore: feedback
Feb 2, 2023
90c6372
Merge branch 'sam/more-bundling' into sam/zod-events
Feb 2, 2023
5cd4cac
Merge branch 'sam/zod-events' into sam/zod-apis
Feb 2, 2023
93e4cb7
move schema validation to the client
Feb 2, 2023
d6eb727
Merge branch 'sam/zod-events' into sam/zod-apis
Feb 2, 2023
80ded23
open api inspired types
Feb 2, 2023
31c7e2e
feat(aws-cdk): make Service generic to support importing a service's …
Feb 2, 2023
af732db
Merge branch 'main' into sam/zod-apis
Feb 2, 2023
bac79f4
class-ify the API contract and unify typed and un-typed
Feb 3, 2023
205c10d
cleaner interface that is backwards compatible
Feb 3, 2023
d472703
cleaning
Feb 3, 2023
04a9032
even better lol
Feb 3, 2023
8e989a5
remove type field and replace with error and status
Feb 3, 2023
c85dd3e
improve type inference and auto complete
Feb 4, 2023
c1aa016
stashing for the night
Feb 4, 2023
794a841
compiling with great type inference and onboarding ramp
Feb 6, 2023
a49e9ae
improvements
Feb 6, 2023
ccae1c9
end-to-end ramp for params
Feb 6, 2023
f97eb54
clean up request
Feb 6, 2023
31f4659
all types working, ready to implement
Feb 6, 2023
4c91205
parsing and validation in api handler
Feb 6, 2023
8a8b6ec
refactor as commands layered on top of raw http
Feb 8, 2023
9191839
update snapshot
Feb 8, 2023
40e280b
add client to @eventual/client
Feb 8, 2023
aee82f0
Merge branch 'main' into sam/zod-apis
Feb 8, 2023
06377bd
revert some noisy changes
Feb 8, 2023
0a6b068
clean up snapshot test
Feb 8, 2023
c3467e2
AWS service client
Feb 8, 2023
c1d7ad6
re-work Service interface for commands and schema generation
Feb 9, 2023
9acb0cb
consolidate and generalize bundling, remove default event handler
Feb 9, 2023
50ba1f2
fix local tests
Feb 9, 2023
ddcd336
fix various deployment bugs
Feb 9, 2023
e43210c
fix spec.json and most tests
Feb 10, 2023
0b8b865
all tests passing
Feb 10, 2023
ea2895b
update snapshots
Feb 10, 2023
3aa1018
uncomment code
Feb 10, 2023
d243847
remove some dead code
Feb 10, 2023
39880da
replace ApiResponse with HTtpResponse in template and docs
Feb 10, 2023
d0892e4
fix create-sst to 1.0
Feb 10, 2023
2ea10f9
feedback
Feb 10, 2023
2b1d2aa
remove log of request and response
Feb 10, 2023
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
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) => {
sam-goodwin marked this conversation as resolved.
Show resolved Hide resolved
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be the default export?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean package.json#main or export default?

I'm also confused by import type * as name from "blah" vs import type name from "blah. Sometimes it complains about no default export. What configuration am i missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant package#main, but ... Are we missing the default export.flag in the tsconfig?


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