Skip to content

Commit 8b90c87

Browse files
authored
Add "inngest/redwood" serve handler (#31)
Adds a new `"inngest/redwood"` import to support the [RedwoodJS](https://redwoodjs.com/) framework, based on #30 to ensure we're covering all bases when adding a new handler. A couple of extra additions are needed: - RedwoodJS [uses Lambda types](https://redwoodjs.com/docs/serverless-functions) to abstract the underlying platform; we can grab the [`aws-lambda`](https://www.npmjs.com/package/aws-lambda) package for this and just import the types. - RedwoodJS performs some interesting redirects in dev, by default exposing [serverless functions](https://redwoodjs.com/docs/serverless-functions) as `/{functionName}` and `/.redwood/functions/{functionName}`, though this can change based on `redwood.toml` configuration (see [Serverless Functions - Developing locally](https://redwoodjs.com/docs/serverless-functions#developing-locally)); it's pertinent here to add the ability to specify a host and path to overwrite the serve URL. The latter point above means there are now two new options for `serve()`: - `serveHost` - The host part of the URL to serve functions from - defaults to the `Host` header value - `servePath` - The path part of the URL to serve functions from - defaults to the `path` passed in the received `APIGatewayProxyEvent`
1 parent 385668d commit 8b90c87

File tree

11 files changed

+404
-11
lines changed

11 files changed

+404
-11
lines changed

etc/inngest.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export interface RegisterOptions {
7373
fetch?: typeof fetch;
7474
inngestRegisterUrl?: string;
7575
landingPage?: boolean;
76+
serveHost?: string;
77+
servePath?: string;
7678
signingKey?: string;
7779
}
7880

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "inngest",
3-
"version": "0.6.0",
3+
"version": "0.6.1-redwood.2",
44
"description": "Official SDK for Inngest.com",
55
"main": "./index.js",
66
"types": "./index.d.ts",
@@ -58,6 +58,7 @@
5858
"devDependencies": {
5959
"@inngest/eslint-plugin": "./rules",
6060
"@microsoft/api-extractor": "^7.31.2",
61+
"@types/aws-lambda": "^8.10.108",
6162
"@types/express": "^4.17.13",
6263
"@types/jest": "^27.4.1",
6364
"@types/sha.js": "^2.4.0",

src/cloudflare.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ class CloudflareCommHandler extends InngestCommHandler {
2626
let isIntrospection: boolean;
2727

2828
try {
29-
reqUrl = new URL(req.url, `https://${req.headers.get("host") || ""}`);
29+
reqUrl = this.reqUrl(
30+
req.url,
31+
`https://${req.headers.get("host") || ""}`
32+
);
3033

3134
isIntrospection = reqUrl.searchParams.has(queryKeys.Introspect);
3235
reqUrl.searchParams.delete(queryKeys.Introspect);

src/express.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export class InngestCommHandler {
128128
*/
129129
protected readonly showLandingPage: boolean | undefined;
130130

131+
protected readonly serveHost: string | undefined;
132+
protected readonly servePath: string | undefined;
133+
131134
/**
132135
* A private collection of functions that are being served. This map is used
133136
* to find and register functions when interacting with Inngest Cloud.
@@ -137,7 +140,14 @@ export class InngestCommHandler {
137140
constructor(
138141
nameOrInngest: string | Inngest<any>,
139142
functions: InngestFunction<any>[],
140-
{ inngestRegisterUrl, fetch, landingPage, signingKey }: RegisterOptions = {}
143+
{
144+
inngestRegisterUrl,
145+
fetch,
146+
landingPage,
147+
signingKey,
148+
serveHost,
149+
servePath,
150+
}: RegisterOptions = {}
141151
) {
142152
this.name =
143153
typeof nameOrInngest === "string" ? nameOrInngest : nameOrInngest.name;
@@ -166,6 +176,8 @@ export class InngestCommHandler {
166176

167177
this.signingKey = signingKey;
168178
this.showLandingPage = landingPage;
179+
this.serveHost = serveHost;
180+
this.servePath = servePath;
169181

170182
this.headers = {
171183
"Content-Type": "application/json",
@@ -200,7 +212,7 @@ export class InngestCommHandler {
200212

201213
let reqUrl;
202214
try {
203-
reqUrl = new URL(req.originalUrl, `${protocol}${hostname || ""}`);
215+
reqUrl = this.reqUrl(req.originalUrl, `${protocol}${hostname || ""}`);
204216
reqUrl.searchParams.delete(queryKeys.Introspect);
205217
} catch (e) {
206218
const message =
@@ -326,6 +338,31 @@ export class InngestCommHandler {
326338
return ["inngest-", `js:v${version}`, ` (${this.frameworkName})`];
327339
}
328340

341+
/**
342+
* Return an Inngest serve endpoint URL given a potential `path` and `host`.
343+
*
344+
* Will automatically use the `serveHost` and `servePath` if they have been
345+
* set when registering.
346+
*/
347+
protected reqUrl(
348+
/**
349+
* The path of the Inngest register URL to create. Regardless of the value,
350+
* will be overwritten by `servePath` if it has been set.
351+
*/
352+
path?: string,
353+
354+
/**
355+
* The host of the Inngest register URL to create. Regardless of the value,
356+
* will be overwritten by `serveHost` if it has been set.
357+
*/
358+
host?: string
359+
): URL {
360+
return new URL(
361+
this.servePath || path?.trim() || "",
362+
this.serveHost || host?.trim() || ""
363+
);
364+
}
365+
329366
protected registerBody(url: URL): RegisterRequest {
330367
const body: RegisterRequest = {
331368
url: url.href,

src/next.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class NextCommHandler extends InngestCommHandler {
2121
try {
2222
const scheme =
2323
process.env.NODE_ENV === "development" ? "http" : "https";
24-
reqUrl = new URL(
24+
reqUrl = this.reqUrl(
2525
req.url as string,
2626
`${scheme}://${req.headers.host || ""}`
2727
);

src/redwood.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as RedwoodHandler from "./redwood";
2+
import { testFramework } from "./test/helpers";
3+
4+
testFramework("Redwood.js", RedwoodHandler, {
5+
transformReq: (req, res, env) => {
6+
return [
7+
{
8+
path: req.path,
9+
headers: req.headers,
10+
httpMethod: req.method,
11+
queryStringParameters: req.query,
12+
body: req.body as string,
13+
},
14+
{},
15+
];
16+
},
17+
18+
// eslint-disable-next-line @typescript-eslint/require-await
19+
transformRes: async (res, ret: RedwoodHandler.RedwoodResponse) => {
20+
return {
21+
status: ret.statusCode,
22+
body: ret.body || "",
23+
headers: ret.headers || {},
24+
};
25+
},
26+
});

src/redwood.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type {
2+
APIGatewayProxyEvent,
3+
Context as LambdaContext,
4+
} from "aws-lambda";
5+
import { z } from "zod";
6+
import {
7+
InngestCommHandler,
8+
serve as defaultServe,
9+
ServeHandler,
10+
} from "./express";
11+
import { envKeys, queryKeys } from "./helpers/consts";
12+
import { devServerUrl } from "./helpers/devserver";
13+
import { devServerHost } from "./helpers/env";
14+
import { landing } from "./landing";
15+
import { IntrospectRequest } from "./types";
16+
17+
export interface RedwoodResponse {
18+
statusCode: number;
19+
body?: string | null;
20+
headers?: Record<string, string>;
21+
}
22+
23+
class RedwoodCommHandler extends InngestCommHandler {
24+
protected override frameworkName = "redwoodjs";
25+
26+
public override createHandler() {
27+
return async (
28+
event: APIGatewayProxyEvent,
29+
context: LambdaContext
30+
): Promise<RedwoodResponse> => {
31+
const headers = { "x-inngest-sdk": this.sdkHeader.join("") };
32+
let reqUrl: URL;
33+
34+
try {
35+
const scheme =
36+
process.env.NODE_ENV === "development" ? "http" : "https";
37+
38+
reqUrl = this.reqUrl(
39+
event.path,
40+
`${scheme}://${event.headers.host || ""}`
41+
);
42+
reqUrl.searchParams.delete(queryKeys.Introspect);
43+
} catch (err) {
44+
return {
45+
statusCode: 500,
46+
body: JSON.stringify(err),
47+
headers,
48+
};
49+
}
50+
51+
if (!this.signingKey && process.env[envKeys.SigningKey]) {
52+
this.signingKey = process.env[envKeys.SigningKey];
53+
}
54+
55+
this._isProd =
56+
process.env.VERCEL_ENV === "production" ||
57+
process.env.CONTEXT === "production" ||
58+
process.env.ENVIRONMENT === "production";
59+
60+
switch (event.httpMethod) {
61+
case "GET": {
62+
const showLandingPage = this.shouldShowLandingPage(
63+
process.env[envKeys.LandingPage]
64+
);
65+
66+
if (!showLandingPage) break;
67+
68+
if (
69+
Object.hasOwnProperty.call(
70+
event.queryStringParameters,
71+
queryKeys.Introspect
72+
)
73+
) {
74+
const introspection: IntrospectRequest = {
75+
...this.registerBody(reqUrl),
76+
devServerURL: devServerUrl(devServerHost()).href,
77+
hasSigningKey: Boolean(this.signingKey),
78+
};
79+
80+
return {
81+
statusCode: 200,
82+
body: JSON.stringify(introspection),
83+
headers,
84+
};
85+
}
86+
87+
// Grab landing page and serve
88+
return {
89+
statusCode: 200,
90+
body: landing,
91+
headers: {
92+
...headers,
93+
"content-type": "text/html; charset=utf-8",
94+
},
95+
};
96+
}
97+
98+
case "PUT": {
99+
// Push config to Inngest.
100+
const { status, message } = await this.register(
101+
reqUrl,
102+
process.env[envKeys.DevServerUrl]
103+
);
104+
105+
return {
106+
statusCode: status,
107+
body: JSON.stringify({ message }),
108+
headers,
109+
};
110+
}
111+
112+
case "POST": {
113+
// Inngest is trying to run a step; confirm signed and run.
114+
const { fnId, stepId } = z
115+
.object({
116+
fnId: z.string().min(1),
117+
stepId: z.string().min(1),
118+
})
119+
.parse({
120+
fnId: event.queryStringParameters?.[queryKeys.FnId],
121+
stepId: event.queryStringParameters?.[queryKeys.StepId],
122+
});
123+
124+
/**
125+
* Some requests can be base64 encoded, requiring us to decode it
126+
* first before parsing as JSON.
127+
*/
128+
const strJson = event.body
129+
? event.isBase64Encoded
130+
? Buffer.from(event.body, "base64").toString()
131+
: event.body
132+
: "{}";
133+
134+
const stepRes = await this.runStep(fnId, stepId, JSON.parse(strJson));
135+
136+
if (stepRes.status === 500) {
137+
return {
138+
statusCode: stepRes.status,
139+
body: JSON.stringify(stepRes.error),
140+
headers,
141+
};
142+
}
143+
144+
return {
145+
statusCode: stepRes.status,
146+
body: JSON.stringify(stepRes.body),
147+
headers,
148+
};
149+
}
150+
}
151+
152+
return {
153+
statusCode: 405,
154+
headers,
155+
};
156+
};
157+
}
158+
}
159+
160+
/**
161+
* In Redwood.js, serve and register any declared functions with Inngest, making
162+
* them available to be triggered by events.
163+
*
164+
* @public
165+
*/
166+
export const serve: ServeHandler = (nameOrInngest, fns, opts): any => {
167+
return defaultServe(new RedwoodCommHandler(nameOrInngest, fns, opts));
168+
};

src/remix.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ class RemixCommHandler extends InngestCommHandler {
3030
let isIntrospection: boolean;
3131

3232
try {
33-
reqUrl = new URL(req.url, `https://${req.headers.get("host") || ""}`);
33+
reqUrl = this.reqUrl(
34+
req.url,
35+
`https://${req.headers.get("host") || ""}`
36+
);
3437
isIntrospection = reqUrl.searchParams.has(queryKeys.Introspect);
3538
reqUrl.searchParams.delete(queryKeys.Introspect);
3639
} catch (err) {

0 commit comments

Comments
 (0)