@tsed/platform-serverless 8.0.1 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ import {pipeline} from "node:stream/promises";
2
+
3
+ import {AnyPromiseResult, AnyToPromise, isSerializable, isStream} from "@tsed/core";
4
+ import {BaseContext, inject, injectable, lazyInject, ProviderScope, runInContext, TokenProvider} from "@tsed/di";
5
+ import {$asyncEmit} from "@tsed/hooks";
6
+ import {serialize} from "@tsed/json-mapper";
7
+ import {DeserializerPipe, PlatformParams, ValidationPipe} from "@tsed/platform-params";
8
+
9
+ import {ServerlessContext} from "../domain/ServerlessContext.js";
10
+ import type {ServerlessEvent} from "../domain/ServerlessEvent.js";
11
+ import {ServerlessResponseStream} from "../domain/ServerlessResponseStream.js";
12
+ import {setResponseHeaders} from "../utils/setResponseHeaders.js";
13
+
14
+ export class PlatformServerlessHandler {
15
+ protected params = inject(PlatformParams);
16
+
17
+ createHandler(token: TokenProvider, propertyKey: string | symbol) {
18
+ const promisedHandler = this.params.compileHandler({token, propertyKey});
19
+
20
+ return ($ctx: ServerlessContext<ServerlessEvent>) => {
21
+ return runInContext($ctx, async () => {
22
+ await $asyncEmit("$onRequest", $ctx);
23
+
24
+ try {
25
+ const resolver = new AnyToPromise();
26
+ const handler = await promisedHandler;
27
+ const result = await resolver.call(() => handler({$ctx}));
28
+
29
+ this.processResult(result, $ctx);
30
+ } catch (er) {
31
+ $ctx.response.status(500).body(er);
32
+ const exceptions = await lazyInject(() => import("@tsed/platform-exceptions"));
33
+
34
+ await exceptions.catch(er, $ctx as unknown as BaseContext);
35
+ }
36
+
37
+ return this.flush($ctx);
38
+ });
39
+ };
40
+ }
41
+
42
+ private async flush($ctx: ServerlessContext<ServerlessEvent>) {
43
+ const body: unknown = $ctx.isHttpEvent() && !$ctx.isAuthorizerEvent() ? await this.makeHttpResponse($ctx) : $ctx.response.getBody();
44
+
45
+ await $asyncEmit("$onResponse", $ctx);
46
+
47
+ $ctx.logger.flush();
48
+ $ctx.destroy();
49
+
50
+ if (!$ctx.isHttpEvent() && $ctx.response.statusCode >= 400 && body) {
51
+ throw new Error((body as Error).message);
52
+ }
53
+
54
+ return body;
55
+ }
56
+
57
+ private async makeHttpResponse($ctx: ServerlessContext<ServerlessEvent>) {
58
+ setResponseHeaders($ctx);
59
+
60
+ let body = $ctx.response.getBody();
61
+
62
+ if (isSerializable(body)) {
63
+ $ctx.response.set("content-type", "application/json");
64
+ body = JSON.stringify(body);
65
+ }
66
+
67
+ const meta = {
68
+ statusCode: $ctx.response.getStatus(),
69
+ headers: {
70
+ ...$ctx.response.getHeaders(),
71
+ "x-request-id": $ctx.id
72
+ }
73
+ };
74
+
75
+ if (isStream(body) && $ctx.responseStream) {
76
+ const resStream = ServerlessResponseStream.setMeta($ctx.responseStream, {
77
+ ...meta,
78
+ headers: {
79
+ "content-type": "application/octet-stream",
80
+ ...meta.headers
81
+ }
82
+ });
83
+
84
+ await pipeline(body, resStream);
85
+
86
+ return undefined;
87
+ }
88
+
89
+ return {
90
+ ...meta,
91
+ body: body === undefined ? "" : body,
92
+ isBase64Encoded: false
93
+ };
94
+ }
95
+
96
+ private processResult({status, headers, data}: AnyPromiseResult, $ctx: ServerlessContext<ServerlessEvent>) {
97
+ if (status) {
98
+ $ctx.response.status(status);
99
+ }
100
+
101
+ if (headers) {
102
+ $ctx.response.setHeaders(headers);
103
+ }
104
+
105
+ if (data !== undefined) {
106
+ data = $ctx.response.getStatus() !== 204 ? data : "";
107
+
108
+ if (isSerializable(data)) {
109
+ data = serialize(data, {
110
+ ...$ctx.endpoint.getResponseOptions($ctx.response.getStatus()),
111
+ useAlias: true,
112
+ endpoint: true
113
+ });
114
+ }
115
+
116
+ $ctx.response.body(data);
117
+ }
118
+ }
119
+ }
120
+
121
+ injectable(PlatformServerlessHandler).scope(ProviderScope.SINGLETON).imports([DeserializerPipe, ValidationPipe]);
@@ -0,0 +1,106 @@
1
+ import {PlatformServerlessTest} from "@tsed/platform-serverless-testing";
2
+
3
+ import {createServerlessContext} from "../../test/utils/createServerlessContext.js";
4
+
5
+ describe("ServerlessContext", () => {
6
+ beforeEach(() => PlatformServerlessTest.create());
7
+ afterEach(() => PlatformServerlessTest.reset());
8
+
9
+ describe("constructor", () => {
10
+ it("should push callback", () => {
11
+ const ctx = createServerlessContext({
12
+ endpoint: {} as any
13
+ });
14
+ expect(ctx.event).toEqual({
15
+ body: "",
16
+ headers: {},
17
+ httpMethod: "",
18
+ isBase64Encoded: false,
19
+ multiValueHeaders: {},
20
+ multiValueQueryStringParameters: {},
21
+ path: "/",
22
+ pathParameters: {},
23
+ queryStringParameters: {},
24
+ requestContext: {
25
+ accountId: "",
26
+ apiId: "",
27
+ httpMethod: "",
28
+ identity: {},
29
+ path: "/",
30
+ protocol: "https",
31
+ requestId: "requestId",
32
+ requestTimeEpoch: 0,
33
+ resourceId: 1,
34
+ resourcePath: "/",
35
+ stage: ""
36
+ },
37
+ resource: "",
38
+ stageVariables: {}
39
+ });
40
+ expect(ctx.response.raw).toEqual(ctx.request.raw);
41
+ expect(ctx.request.event).toEqual(ctx.response.event);
42
+ expect(ctx.request.response).toEqual(ctx.response);
43
+ expect(ctx.response.request).toEqual(ctx.request);
44
+ expect(ctx.isHttpEvent()).toBeTruthy();
45
+ expect(ctx.isAuthorizerEvent()).toBeFalsy();
46
+ });
47
+ it("should push callback with event headers", () => {
48
+ const ctx = createServerlessContext({
49
+ endpoint: {} as any,
50
+ event: {
51
+ headers: undefined
52
+ }
53
+ });
54
+ expect(ctx.event).toEqual({
55
+ body: "",
56
+ headers: {},
57
+ httpMethod: "",
58
+ isBase64Encoded: false,
59
+ multiValueHeaders: {},
60
+ multiValueQueryStringParameters: {},
61
+ path: "/",
62
+ pathParameters: {},
63
+ queryStringParameters: {},
64
+ requestContext: {
65
+ accountId: "",
66
+ apiId: "",
67
+ httpMethod: "",
68
+ identity: {},
69
+ path: "/",
70
+ protocol: "https",
71
+ requestId: "requestId",
72
+ requestTimeEpoch: 0,
73
+ resourceId: 1,
74
+ resourcePath: "/",
75
+ stage: ""
76
+ },
77
+ resource: "",
78
+ stageVariables: {}
79
+ });
80
+ expect(ctx.response.raw).toEqual(ctx.request.raw);
81
+ expect(ctx.request.event).toEqual(ctx.response.event);
82
+ expect(ctx.request.response).toEqual(ctx.response);
83
+ expect(ctx.response.request).toEqual(ctx.request);
84
+ });
85
+ it("should accept auth event (request)", () => {
86
+ const ctx = createServerlessContext({
87
+ event: {
88
+ type: "REQUEST",
89
+ httpMethod: "GET"
90
+ } as never,
91
+ endpoint: {} as any
92
+ });
93
+ expect(ctx.isHttpEvent()).toBeTruthy();
94
+ expect(ctx.isAuthorizerEvent()).toBeTruthy();
95
+ });
96
+ it("should accept auth event (token)", () => {
97
+ const ctx = createServerlessContext({
98
+ event: {
99
+ type: "TOKEN"
100
+ } as never,
101
+ endpoint: {} as any
102
+ });
103
+ expect(ctx.isAuthorizerEvent()).toBeTruthy();
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,60 @@
1
+ import {DIContext, DIContextOptions} from "@tsed/di";
2
+ import {JsonEntityStore} from "@tsed/schema";
3
+ import type {APIGatewayProxyEvent, Context} from "aws-lambda";
4
+
5
+ import {ServerlessRequest} from "./ServerlessRequest.js";
6
+ import {ServerlessResponse} from "./ServerlessResponse.js";
7
+ import type {ServerlessResponseStream} from "./ServerlessResponseStream.js";
8
+
9
+ export interface ServerlessContextOptions<Event = APIGatewayProxyEvent> extends DIContextOptions {
10
+ event: Event;
11
+ context: Context;
12
+ responseStream?: ServerlessResponseStream;
13
+ endpoint: JsonEntityStore;
14
+ }
15
+
16
+ export class ServerlessContext<Event extends object = APIGatewayProxyEvent> extends DIContext {
17
+ readonly response: ServerlessResponse<Event>;
18
+ readonly request: ServerlessRequest<Event>;
19
+ readonly context: Context;
20
+ readonly event: Event;
21
+ readonly responseStream: ServerlessResponseStream | undefined;
22
+ readonly endpoint: JsonEntityStore;
23
+ readonly PLATFORM = "SERVERLESS";
24
+
25
+ constructor({event, context, endpoint, responseStream, ...options}: ServerlessContextOptions<Event>) {
26
+ super({
27
+ ...options,
28
+ maxStackSize: 0
29
+ });
30
+ this.context = context;
31
+ this.responseStream = responseStream;
32
+ this.event = {
33
+ ...event,
34
+ headers:
35
+ "headers" in event && event.headers
36
+ ? Object.fromEntries(Object.entries(event.headers).map(([key, value]) => [key.toLowerCase(), value]))
37
+ : {}
38
+ };
39
+ this.request = new ServerlessRequest<Event>(this);
40
+ this.response = new ServerlessResponse<Event>(this);
41
+ this.endpoint = endpoint;
42
+ }
43
+
44
+ isHttpEvent() {
45
+ return "httpMethod" in this.event;
46
+ }
47
+
48
+ isAuthorizerEvent() {
49
+ return "type" in this.event && ["TOKEN", "REQUEST"].includes(this.event.type as string);
50
+ }
51
+
52
+ async destroy() {
53
+ await super.destroy();
54
+ this.response.destroy();
55
+ }
56
+
57
+ isDone() {
58
+ return this.response.isDone();
59
+ }
60
+ }
@@ -0,0 +1,24 @@
1
+ import type {
2
+ APIGatewayProxyEvent,
3
+ APIGatewayProxyEventV2,
4
+ APIGatewayTokenAuthorizerEvent,
5
+ CloudWatchLogsEvent,
6
+ DynamoDBStreamEvent,
7
+ KinesisStreamEvent,
8
+ S3Event,
9
+ ScheduledEvent,
10
+ SNSEvent,
11
+ SQSEvent
12
+ } from "aws-lambda";
13
+
14
+ export type ServerlessEvent =
15
+ | APIGatewayProxyEvent
16
+ | APIGatewayProxyEventV2
17
+ | S3Event
18
+ | SQSEvent
19
+ | SNSEvent
20
+ | DynamoDBStreamEvent
21
+ | KinesisStreamEvent
22
+ | CloudWatchLogsEvent
23
+ | ScheduledEvent
24
+ | APIGatewayTokenAuthorizerEvent;
@@ -0,0 +1,130 @@
1
+ import {PlatformServerlessTest} from "@tsed/platform-serverless-testing";
2
+
3
+ import {createServerlessContext} from "../../test/utils/createServerlessContext.js";
4
+
5
+ describe("ServerlessRequest", () => {
6
+ beforeEach(() => PlatformServerlessTest.create());
7
+ afterEach(() => PlatformServerlessTest.reset());
8
+ describe("secure()", () => {
9
+ it("should return expected value", () => {
10
+ const context = createServerlessContext({
11
+ endpoint: {} as any
12
+ });
13
+
14
+ expect(context.request.secure).toEqual(true);
15
+ });
16
+ });
17
+
18
+ describe("host()", () => {
19
+ it("should return expected value", () => {
20
+ const context = createServerlessContext({
21
+ endpoint: {} as any
22
+ });
23
+ context.event.headers["host"] = "host";
24
+
25
+ expect(context.request.host).toEqual("host");
26
+ });
27
+ });
28
+ describe("protocol()", () => {
29
+ it("should return expected value", () => {
30
+ const context = createServerlessContext({
31
+ endpoint: {} as any
32
+ });
33
+
34
+ expect(context.request.protocol).toEqual("https");
35
+ });
36
+ });
37
+ describe("url()", () => {
38
+ it("should return expected value", () => {
39
+ const context = createServerlessContext({
40
+ endpoint: {} as any
41
+ });
42
+
43
+ expect(context.request.url).toEqual("/");
44
+ });
45
+ });
46
+ describe("headers()", () => {
47
+ it("should return expected value", () => {
48
+ const context = createServerlessContext({
49
+ endpoint: {} as any
50
+ });
51
+
52
+ expect(context.request.headers).toEqual({});
53
+ });
54
+ });
55
+ describe("method()", () => {
56
+ it("should return expected value", () => {
57
+ const context = createServerlessContext({
58
+ endpoint: {} as any
59
+ });
60
+ context.request.raw.httpMethod = "GET";
61
+ expect(context.request.method).toEqual("GET");
62
+ });
63
+ });
64
+
65
+ describe("body()", () => {
66
+ it("should return expected value", () => {
67
+ const context = createServerlessContext({
68
+ endpoint: {} as any
69
+ });
70
+
71
+ expect(context.request.body).toEqual({});
72
+ });
73
+ it("should return catch error on body", () => {
74
+ const context = createServerlessContext({
75
+ endpoint: {} as any
76
+ });
77
+ context.request.raw.body = "{";
78
+
79
+ expect(context.request.body).toEqual("{");
80
+ });
81
+ it("should return parse body", () => {
82
+ const context = createServerlessContext({
83
+ endpoint: {} as any
84
+ });
85
+ context.request.raw.body = "{}";
86
+
87
+ expect(context.request.body).toEqual({});
88
+ });
89
+ });
90
+
91
+ describe("rawBody()", () => {
92
+ it("should return expected value", () => {
93
+ const context = createServerlessContext({
94
+ endpoint: {} as any
95
+ });
96
+
97
+ expect(context.request.rawBody).toEqual("");
98
+ });
99
+ });
100
+
101
+ describe("params()", () => {
102
+ it("should return expected value", () => {
103
+ const context = createServerlessContext({
104
+ endpoint: {} as any
105
+ });
106
+
107
+ expect(context.request.params).toEqual({});
108
+ });
109
+
110
+ it("should return expected value (undefined params)", () => {
111
+ const context = createServerlessContext({
112
+ endpoint: {} as any
113
+ });
114
+
115
+ delete context.event.pathParameters;
116
+
117
+ expect(context.request.params).toEqual({});
118
+ });
119
+ });
120
+
121
+ describe("query()", () => {
122
+ it("should return expected value", () => {
123
+ const context = createServerlessContext({
124
+ endpoint: {} as any
125
+ });
126
+
127
+ expect(context.request.query).toEqual({});
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,106 @@
1
+ import {getValue} from "@tsed/core";
2
+ import type {APIGatewayProxyEvent} from "aws-lambda";
3
+
4
+ import {ServerlessContext} from "./ServerlessContext.js";
5
+
6
+ /**
7
+ * @platform
8
+ */
9
+ export class ServerlessRequest<Event extends object = APIGatewayProxyEvent> {
10
+ constructor(protected $ctx: ServerlessContext<Event>) {}
11
+
12
+ /**
13
+ * APIGatewayProxyEvent to no break the current code
14
+ * @note use getEvent() instead to use generic type
15
+ */
16
+ get event(): APIGatewayProxyEvent {
17
+ return this.getEvent();
18
+ }
19
+
20
+ get raw(): Event {
21
+ return this.event as unknown as Event;
22
+ }
23
+
24
+ get response() {
25
+ return this.$ctx.response;
26
+ }
27
+
28
+ get secure(): boolean {
29
+ return true;
30
+ }
31
+
32
+ get host(): string {
33
+ return this.get("host");
34
+ }
35
+
36
+ get protocol(): string {
37
+ return "https";
38
+ }
39
+
40
+ /**
41
+ * Get the url of the request.
42
+ *
43
+ * Is equivalent of `express.response.originalUrl || express.response.url`.
44
+ */
45
+ get url(): string {
46
+ return this.event.path;
47
+ }
48
+
49
+ get headers() {
50
+ return this.event.headers;
51
+ }
52
+
53
+ get method(): string {
54
+ return this.event.httpMethod;
55
+ }
56
+
57
+ /**
58
+ * Contains key-value pairs of data submitted in the request body. By default, it is `undefined`, and is populated when you use
59
+ * `body-parsing` middleware such as `express.json()` or `express.urlencoded()`.
60
+ */
61
+ get body(): any {
62
+ try {
63
+ return this.event.body ? JSON.parse(this.event.body!) : {};
64
+ } catch (er) {
65
+ return this.event.body;
66
+ }
67
+ }
68
+
69
+ get rawBody(): any {
70
+ return this.event.body;
71
+ }
72
+
73
+ /**
74
+ * This property is an object containing properties mapped to the named route `parameters`.
75
+ * For example, if you have the route `/user/:name`, then the `name` property is available as `req.params.name`.
76
+ * This object defaults to `{}`.
77
+ */
78
+ get params(): {[key: string]: any} {
79
+ return this.event.pathParameters || {};
80
+ }
81
+
82
+ /**
83
+ * This property is an object containing a property for each query string parameter in the route.
84
+ * When query parser is set to disabled, it is an empty object `{}`, otherwise it is the result of the configured query parser.
85
+ */
86
+ get query(): {[key: string]: any} {
87
+ return this.event.queryStringParameters || {};
88
+ }
89
+
90
+ getEvent<E = Event>(): E {
91
+ return this.$ctx.event as unknown as E;
92
+ }
93
+
94
+ /**
95
+ * Returns the HTTP request header specified by field. The match is case-insensitive.
96
+ *
97
+ * ```typescript
98
+ * request.get('Content-Type') // => "text/plain"
99
+ * ```
100
+ *
101
+ * @param name
102
+ */
103
+ get(name: string) {
104
+ return getValue(this.event.headers, name);
105
+ }
106
+ }
@@ -0,0 +1,139 @@
1
+ import {PlatformServerlessTest} from "@tsed/platform-serverless-testing";
2
+
3
+ import {createServerlessContext} from "../../test/utils/createServerlessContext.js";
4
+
5
+ describe("ServerlessResponse", () => {
6
+ beforeEach(() => PlatformServerlessTest.create());
7
+ afterEach(() => PlatformServerlessTest.reset());
8
+
9
+ describe("isHeadersSent", () => {
10
+ it("should push callback", () => {
11
+ const context = createServerlessContext({
12
+ endpoint: {} as any
13
+ });
14
+ expect(context.response.isHeadersSent).toEqual(false);
15
+
16
+ context.response.destroy();
17
+
18
+ expect(context.response.isHeadersSent).toEqual(true);
19
+ });
20
+ });
21
+ describe("statusCode", () => {
22
+ it("should return the statusCode", () => {
23
+ const context = createServerlessContext({
24
+ endpoint: {} as any
25
+ });
26
+ context.response.statusCode = 201;
27
+ expect(context.response.statusCode).toEqual(201);
28
+ });
29
+ });
30
+ describe("locals()", () => {
31
+ it("should return the locals", () => {
32
+ const context = createServerlessContext({
33
+ endpoint: {} as any
34
+ });
35
+
36
+ expect(context.response.locals).toEqual({});
37
+ });
38
+ });
39
+ describe("contentLength()", () => {
40
+ it("should return the content length", () => {
41
+ const context = createServerlessContext({
42
+ endpoint: {} as any
43
+ });
44
+ context.response.contentLength(300);
45
+ expect(context.response.getContentLength()).toEqual(300);
46
+ });
47
+
48
+ it("should return the default content length", () => {
49
+ const context = createServerlessContext({
50
+ endpoint: {} as any
51
+ });
52
+ context.response.contentLength(" " as any);
53
+ expect(context.response.getContentLength()).toEqual(0);
54
+ });
55
+ });
56
+ describe("location()", () => {
57
+ it("should set location", () => {
58
+ const context = createServerlessContext({
59
+ endpoint: {} as any
60
+ });
61
+
62
+ context.response.location("/path/to");
63
+
64
+ expect(context.response.getHeaders()).toEqual({
65
+ location: "/path/to"
66
+ });
67
+ });
68
+ it("should set location (back)", () => {
69
+ const context = createServerlessContext({
70
+ endpoint: {} as any
71
+ });
72
+
73
+ context.request.headers["Referrer"] = "https://referrer.com";
74
+ context.response.location("back");
75
+
76
+ expect(context.response.getHeaders()).toEqual({
77
+ location: "https://referrer.com"
78
+ });
79
+ });
80
+ });
81
+ describe("redirect()", () => {
82
+ it("should set location", () => {
83
+ const context = createServerlessContext({
84
+ endpoint: {} as any
85
+ });
86
+
87
+ context.response.redirect(301, "/path/to");
88
+
89
+ expect(context.response.getHeaders()).toEqual({
90
+ "content-length": 42,
91
+ location: "/path/to"
92
+ });
93
+ expect(context.response.statusCode).toEqual(301);
94
+ expect(context.response.getBody()).toEqual("Moved Permanently. Redirecting to /path/to");
95
+ });
96
+ it("should set location (back)", () => {
97
+ const context = createServerlessContext({
98
+ endpoint: {} as any
99
+ });
100
+
101
+ context.request.headers["Referrer"] = "https://referrer.com";
102
+ context.response.redirect(301, "back");
103
+
104
+ expect(context.response.getHeaders()).toEqual({
105
+ "content-length": 54,
106
+ location: "https://referrer.com"
107
+ });
108
+ expect(context.response.statusCode).toEqual(301);
109
+ expect(context.response.getStatus()).toEqual(301);
110
+ expect(context.response.getContentLength()).toEqual(54);
111
+ expect(context.response.getContentType()).toEqual("");
112
+ expect(context.response.getBody()).toEqual("Moved Permanently. Redirecting to https://referrer.com");
113
+ });
114
+ });
115
+ describe("setHeader()", () => {
116
+ it("should set location", () => {
117
+ const context = createServerlessContext({
118
+ endpoint: {} as any
119
+ });
120
+
121
+ context.event.headers["Referrer"] = "https://referrer.com";
122
+ context.response.set("location", "back");
123
+ expect(context.response.getHeaders()).toEqual({
124
+ location: "https://referrer.com"
125
+ });
126
+ });
127
+ it("should set location back with default value", () => {
128
+ const context = createServerlessContext({
129
+ endpoint: {} as any
130
+ });
131
+
132
+ context.event.headers["Referrer"] = "";
133
+ context.response.set("location", "back");
134
+ expect(context.response.getHeaders()).toEqual({
135
+ location: "/"
136
+ });
137
+ });
138
+ });
139
+ });