@hazeljs/serverless 0.2.0-alpha.1
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.
- package/LICENSE +192 -0
- package/README.md +493 -0
- package/dist/adapters.test.d.ts +2 -0
- package/dist/adapters.test.d.ts.map +1 -0
- package/dist/adapters.test.js +432 -0
- package/dist/cloud-function.adapter.d.ts +109 -0
- package/dist/cloud-function.adapter.d.ts.map +1 -0
- package/dist/cloud-function.adapter.js +271 -0
- package/dist/cold-start.optimizer.d.ts +70 -0
- package/dist/cold-start.optimizer.d.ts.map +1 -0
- package/dist/cold-start.optimizer.js +202 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/integration.test.d.ts +6 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +191 -0
- package/dist/lambda.adapter.d.ts +102 -0
- package/dist/lambda.adapter.d.ts.map +1 -0
- package/dist/lambda.adapter.js +258 -0
- package/dist/serverless.decorator.d.ts +166 -0
- package/dist/serverless.decorator.d.ts.map +1 -0
- package/dist/serverless.decorator.js +56 -0
- package/dist/serverless.test.d.ts +2 -0
- package/dist/serverless.test.d.ts.map +1 -0
- package/dist/serverless.test.js +246 -0
- package/package.json +52 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
/**
|
|
16
|
+
* Integration tests: real HazelJS app through Lambda and Cloud Function adapters.
|
|
17
|
+
* No mocks of @hazeljs/core — uses real HazelApp, router, and controllers.
|
|
18
|
+
*/
|
|
19
|
+
require("reflect-metadata");
|
|
20
|
+
const core_1 = require("@hazeljs/core");
|
|
21
|
+
const lambda_adapter_1 = require("./lambda.adapter");
|
|
22
|
+
const cloud_function_adapter_1 = require("./cloud-function.adapter");
|
|
23
|
+
const cold_start_optimizer_1 = require("./cold-start.optimizer");
|
|
24
|
+
// ─── Minimal app for integration ───────────────────────────────────────────
|
|
25
|
+
let IntegrationController = class IntegrationController {
|
|
26
|
+
ping() {
|
|
27
|
+
return { ok: true, message: 'pong' };
|
|
28
|
+
}
|
|
29
|
+
getById(id) {
|
|
30
|
+
return { id };
|
|
31
|
+
}
|
|
32
|
+
echo(body) {
|
|
33
|
+
return { echoed: body };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
__decorate([
|
|
37
|
+
(0, core_1.Get)('/ping'),
|
|
38
|
+
__metadata("design:type", Function),
|
|
39
|
+
__metadata("design:paramtypes", []),
|
|
40
|
+
__metadata("design:returntype", void 0)
|
|
41
|
+
], IntegrationController.prototype, "ping", null);
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, core_1.Get)('/params/:id'),
|
|
44
|
+
__param(0, (0, core_1.Param)('id')),
|
|
45
|
+
__metadata("design:type", Function),
|
|
46
|
+
__metadata("design:paramtypes", [String]),
|
|
47
|
+
__metadata("design:returntype", void 0)
|
|
48
|
+
], IntegrationController.prototype, "getById", null);
|
|
49
|
+
__decorate([
|
|
50
|
+
(0, core_1.Post)('/echo'),
|
|
51
|
+
__param(0, (0, core_1.Body)()),
|
|
52
|
+
__metadata("design:type", Function),
|
|
53
|
+
__metadata("design:paramtypes", [Object]),
|
|
54
|
+
__metadata("design:returntype", void 0)
|
|
55
|
+
], IntegrationController.prototype, "echo", null);
|
|
56
|
+
IntegrationController = __decorate([
|
|
57
|
+
(0, core_1.Controller)('/api')
|
|
58
|
+
], IntegrationController);
|
|
59
|
+
let IntegrationTestModule = class IntegrationTestModule {
|
|
60
|
+
};
|
|
61
|
+
IntegrationTestModule = __decorate([
|
|
62
|
+
(0, core_1.HazelModule)({
|
|
63
|
+
controllers: [IntegrationController],
|
|
64
|
+
})
|
|
65
|
+
], IntegrationTestModule);
|
|
66
|
+
// ─── Lambda helpers ──────────────────────────────────────────────────────────
|
|
67
|
+
function makeLambdaEvent(overrides = {}) {
|
|
68
|
+
return {
|
|
69
|
+
httpMethod: 'GET',
|
|
70
|
+
path: '/api/ping',
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
queryStringParameters: null,
|
|
73
|
+
body: null,
|
|
74
|
+
isBase64Encoded: false,
|
|
75
|
+
...overrides,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function makeLambdaContext(overrides = {}) {
|
|
79
|
+
return {
|
|
80
|
+
awsRequestId: 'int-test-req-1',
|
|
81
|
+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:000:function:integration-test',
|
|
82
|
+
functionName: 'integration-test',
|
|
83
|
+
functionVersion: '$LATEST',
|
|
84
|
+
getRemainingTimeInMillis: () => 30000,
|
|
85
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
86
|
+
...overrides,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// ─── Cloud Function helpers ──────────────────────────────────────────────────
|
|
90
|
+
function makeCloudReq(overrides = {}) {
|
|
91
|
+
return {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
url: '/api/ping',
|
|
94
|
+
path: '/api/ping',
|
|
95
|
+
headers: { 'content-type': 'application/json' },
|
|
96
|
+
query: {},
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function makeCloudRes() {
|
|
101
|
+
const state = {
|
|
102
|
+
_status: 200,
|
|
103
|
+
_body: undefined,
|
|
104
|
+
_headers: {},
|
|
105
|
+
};
|
|
106
|
+
return Object.assign(state, {
|
|
107
|
+
status(code) {
|
|
108
|
+
state._status = code;
|
|
109
|
+
return this;
|
|
110
|
+
},
|
|
111
|
+
set(field, value) {
|
|
112
|
+
state._headers[field] = value;
|
|
113
|
+
return this;
|
|
114
|
+
},
|
|
115
|
+
send(body) {
|
|
116
|
+
state._body = body;
|
|
117
|
+
},
|
|
118
|
+
json(body) {
|
|
119
|
+
state._body = body;
|
|
120
|
+
},
|
|
121
|
+
end() { },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ─── Integration tests ───────────────────────────────────────────────────────
|
|
125
|
+
describe('integration (real HazelApp)', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
128
|
+
});
|
|
129
|
+
describe('createLambdaHandler', () => {
|
|
130
|
+
const handler = (0, lambda_adapter_1.createLambdaHandler)(IntegrationTestModule);
|
|
131
|
+
it('GET /api/ping returns 200 and pong', async () => {
|
|
132
|
+
const result = await handler(makeLambdaEvent({ httpMethod: 'GET', path: '/api/ping' }), makeLambdaContext());
|
|
133
|
+
expect(result.statusCode).toBe(200);
|
|
134
|
+
const data = JSON.parse(result.body);
|
|
135
|
+
expect(data).toEqual({ ok: true, message: 'pong' });
|
|
136
|
+
});
|
|
137
|
+
it('GET /api/params/123 returns 200 and id', async () => {
|
|
138
|
+
const result = await handler(makeLambdaEvent({
|
|
139
|
+
httpMethod: 'GET',
|
|
140
|
+
path: '/api/params/123',
|
|
141
|
+
pathParameters: { id: '123' },
|
|
142
|
+
}), makeLambdaContext());
|
|
143
|
+
expect(result.statusCode).toBe(200);
|
|
144
|
+
const data = JSON.parse(result.body);
|
|
145
|
+
expect(data).toEqual({ id: '123' });
|
|
146
|
+
});
|
|
147
|
+
it('POST /api/echo returns 200 and echoed body', async () => {
|
|
148
|
+
const result = await handler(makeLambdaEvent({
|
|
149
|
+
httpMethod: 'POST',
|
|
150
|
+
path: '/api/echo',
|
|
151
|
+
body: JSON.stringify({ x: 1, text: 'hello' }),
|
|
152
|
+
}), makeLambdaContext());
|
|
153
|
+
expect(result.statusCode).toBe(200);
|
|
154
|
+
const data = JSON.parse(result.body);
|
|
155
|
+
expect(data).toEqual({ echoed: { x: 1, text: 'hello' } });
|
|
156
|
+
});
|
|
157
|
+
it('GET /api/missing returns 404', async () => {
|
|
158
|
+
const result = await handler(makeLambdaEvent({ httpMethod: 'GET', path: '/api/missing' }), makeLambdaContext());
|
|
159
|
+
expect(result.statusCode).toBe(404);
|
|
160
|
+
const data = JSON.parse(result.body);
|
|
161
|
+
expect(data.message).toBe('Route not found');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('createCloudFunctionHandler', () => {
|
|
165
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionHandler)(IntegrationTestModule);
|
|
166
|
+
it('GET /api/ping returns 200 and pong', async () => {
|
|
167
|
+
const res = makeCloudRes();
|
|
168
|
+
await handler(makeCloudReq({ method: 'GET', url: '/api/ping', path: '/api/ping' }), res);
|
|
169
|
+
expect(res._status).toBe(200);
|
|
170
|
+
const body = typeof res._body === 'string' ? JSON.parse(res._body) : res._body;
|
|
171
|
+
expect(body).toEqual({ ok: true, message: 'pong' });
|
|
172
|
+
});
|
|
173
|
+
it('POST /api/echo returns 200 and echoed body', async () => {
|
|
174
|
+
const res = makeCloudRes();
|
|
175
|
+
await handler(makeCloudReq({
|
|
176
|
+
method: 'POST',
|
|
177
|
+
url: '/api/echo',
|
|
178
|
+
path: '/api/echo',
|
|
179
|
+
body: { x: 2, text: 'world' },
|
|
180
|
+
}), res);
|
|
181
|
+
expect(res._status).toBe(200);
|
|
182
|
+
const body = typeof res._body === 'string' ? JSON.parse(res._body) : res._body;
|
|
183
|
+
expect(body).toEqual({ echoed: { x: 2, text: 'world' } });
|
|
184
|
+
});
|
|
185
|
+
it('GET /api/missing returns 404', async () => {
|
|
186
|
+
const res = makeCloudRes();
|
|
187
|
+
await handler(makeCloudReq({ method: 'GET', url: '/api/missing', path: '/api/missing' }), res);
|
|
188
|
+
expect(res._status).toBe(404);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { HazelApp } from '@hazeljs/core';
|
|
2
|
+
import { Type } from '@hazeljs/core';
|
|
3
|
+
import { ServerlessEvent, ServerlessResponse, ServerlessContext } from './serverless.decorator';
|
|
4
|
+
/**
|
|
5
|
+
* AWS Lambda event types
|
|
6
|
+
*/
|
|
7
|
+
export interface LambdaEvent extends ServerlessEvent {
|
|
8
|
+
resource?: string;
|
|
9
|
+
pathParameters?: Record<string, string>;
|
|
10
|
+
stageVariables?: Record<string, string>;
|
|
11
|
+
requestContext?: {
|
|
12
|
+
accountId: string;
|
|
13
|
+
apiId: string;
|
|
14
|
+
protocol: string;
|
|
15
|
+
httpMethod: string;
|
|
16
|
+
path: string;
|
|
17
|
+
stage: string;
|
|
18
|
+
requestId: string;
|
|
19
|
+
requestTime: string;
|
|
20
|
+
requestTimeEpoch: number;
|
|
21
|
+
identity: {
|
|
22
|
+
sourceIp: string;
|
|
23
|
+
userAgent: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* AWS Lambda context
|
|
29
|
+
*/
|
|
30
|
+
export interface LambdaContext extends ServerlessContext {
|
|
31
|
+
awsRequestId: string;
|
|
32
|
+
invokedFunctionArn: string;
|
|
33
|
+
callbackWaitsForEmptyEventLoop: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Options for createLambdaHandler
|
|
37
|
+
*/
|
|
38
|
+
export interface LambdaHandlerOptions {
|
|
39
|
+
/** Called after the HazelJS app is initialized (e.g. for custom setup). */
|
|
40
|
+
onInit?: (app: HazelApp) => Promise<void>;
|
|
41
|
+
/** Called when the handler throws (e.g. for custom logging or reporting). */
|
|
42
|
+
onError?: (error: unknown) => void;
|
|
43
|
+
/** MIME types (e.g. 'image/png', 'image/*') that trigger base64 encoding of Buffer body for Lambda. */
|
|
44
|
+
binaryMimeTypes?: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lambda adapter for HazelJS
|
|
48
|
+
*/
|
|
49
|
+
export declare class LambdaAdapter {
|
|
50
|
+
private moduleClass;
|
|
51
|
+
private options;
|
|
52
|
+
private app?;
|
|
53
|
+
private optimizer;
|
|
54
|
+
private isColdStart;
|
|
55
|
+
constructor(moduleClass: Type<unknown>, options?: LambdaHandlerOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Initialize the HazelJS application
|
|
58
|
+
*/
|
|
59
|
+
private initialize;
|
|
60
|
+
/**
|
|
61
|
+
* Create Lambda handler
|
|
62
|
+
*/
|
|
63
|
+
createHandler(): (event: LambdaEvent, context: LambdaContext) => Promise<ServerlessResponse>;
|
|
64
|
+
/**
|
|
65
|
+
* Convert Lambda event to HTTP request format
|
|
66
|
+
*/
|
|
67
|
+
private convertLambdaEventToRequest;
|
|
68
|
+
/**
|
|
69
|
+
* Parse request body
|
|
70
|
+
*/
|
|
71
|
+
private parseBody;
|
|
72
|
+
/**
|
|
73
|
+
* Process request through HazelJS router
|
|
74
|
+
*/
|
|
75
|
+
private processRequest;
|
|
76
|
+
/**
|
|
77
|
+
* Whether the given Content-Type matches any of the binaryMimeTypes patterns.
|
|
78
|
+
*/
|
|
79
|
+
private isBinaryContentType;
|
|
80
|
+
/**
|
|
81
|
+
* Get application instance
|
|
82
|
+
*/
|
|
83
|
+
getApp(): HazelApp | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Check if this is a cold start
|
|
86
|
+
*/
|
|
87
|
+
isCold(): boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a Lambda handler for a HazelJS module
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // handler.ts
|
|
95
|
+
* import { createLambdaHandler } from '@hazeljs/core';
|
|
96
|
+
* import { AppModule } from './app.module';
|
|
97
|
+
*
|
|
98
|
+
* export const handler = createLambdaHandler(AppModule);
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createLambdaHandler(moduleClass: Type<unknown>, options?: LambdaHandlerOptions): (event: LambdaEvent, context: LambdaContext) => Promise<ServerlessResponse>;
|
|
102
|
+
//# sourceMappingURL=lambda.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lambda.adapter.d.ts","sourceRoot":"","sources":["../src/lambda.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,wBAAwB,CAAC;AAGhC;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,cAAc,CAAC,EAAE;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE;YACR,QAAQ,EAAE,MAAM,CAAC;YACjB,SAAS,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,8BAA8B,EAAE,OAAO,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,uGAAuG;IACvG,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,aAAa;IAMtB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,OAAO;IANjB,OAAO,CAAC,GAAG,CAAC,CAAW;IACvB,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,WAAW,CAAQ;gBAGjB,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,EAC1B,OAAO,GAAE,oBAAyB;IAK5C;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACH,aAAa,KACG,OAAO,WAAW,EAAE,SAAS,aAAa,KAAG,OAAO,CAAC,kBAAkB,CAAC;IAmCxF;;OAEG;IACH,OAAO,CAAC,2BAA2B;IA+BnC;;OAEG;IACH,OAAO,CAAC,SAAS;IAYjB;;OAEG;YACW,cAAc;IAqI5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAc3B;;OAEG;IACH,MAAM,IAAI,QAAQ,GAAG,SAAS;IAI9B;;OAEG;IACH,MAAM,IAAI,OAAO;CAGlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,EAC1B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAG7E"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LambdaAdapter = void 0;
|
|
7
|
+
exports.createLambdaHandler = createLambdaHandler;
|
|
8
|
+
const core_1 = require("@hazeljs/core");
|
|
9
|
+
const core_2 = __importDefault(require("@hazeljs/core"));
|
|
10
|
+
const serverless_decorator_1 = require("./serverless.decorator");
|
|
11
|
+
const cold_start_optimizer_1 = require("./cold-start.optimizer");
|
|
12
|
+
/**
|
|
13
|
+
* Lambda adapter for HazelJS
|
|
14
|
+
*/
|
|
15
|
+
class LambdaAdapter {
|
|
16
|
+
constructor(moduleClass, options = {}) {
|
|
17
|
+
this.moduleClass = moduleClass;
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.isColdStart = true;
|
|
20
|
+
this.optimizer = cold_start_optimizer_1.ColdStartOptimizer.getInstance();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the HazelJS application
|
|
24
|
+
*/
|
|
25
|
+
async initialize() {
|
|
26
|
+
if (this.app) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
core_2.default.info('Initializing HazelJS application for Lambda...');
|
|
31
|
+
// Check if cold start optimization is enabled
|
|
32
|
+
const metadata = (0, serverless_decorator_1.getServerlessMetadata)(this.moduleClass);
|
|
33
|
+
if (metadata?.coldStartOptimization) {
|
|
34
|
+
await this.optimizer.warmUp();
|
|
35
|
+
}
|
|
36
|
+
// Create HazelJS application
|
|
37
|
+
this.app = new core_1.HazelApp(this.moduleClass);
|
|
38
|
+
if (this.options.onInit && this.app) {
|
|
39
|
+
await this.options.onInit(this.app);
|
|
40
|
+
}
|
|
41
|
+
const duration = Date.now() - startTime;
|
|
42
|
+
core_2.default.info(`Lambda initialization completed in ${duration}ms`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create Lambda handler
|
|
46
|
+
*/
|
|
47
|
+
createHandler() {
|
|
48
|
+
return async (event, context) => {
|
|
49
|
+
try {
|
|
50
|
+
// Log cold start
|
|
51
|
+
if (this.isColdStart) {
|
|
52
|
+
core_2.default.info('Lambda cold start detected');
|
|
53
|
+
this.isColdStart = false;
|
|
54
|
+
}
|
|
55
|
+
// Initialize application
|
|
56
|
+
await this.initialize();
|
|
57
|
+
// Convert Lambda event to HTTP request
|
|
58
|
+
const request = this.convertLambdaEventToRequest(event, context);
|
|
59
|
+
// Process request through HazelJS
|
|
60
|
+
const response = await this.processRequest(request);
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.options.onError?.(error);
|
|
65
|
+
core_2.default.error('Lambda handler error:', error);
|
|
66
|
+
return {
|
|
67
|
+
statusCode: 500,
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
error: 'Internal Server Error',
|
|
70
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
71
|
+
}),
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convert Lambda event to HTTP request format
|
|
81
|
+
*/
|
|
82
|
+
convertLambdaEventToRequest(event, context) {
|
|
83
|
+
return {
|
|
84
|
+
method: event.httpMethod || 'GET',
|
|
85
|
+
url: event.path || '/',
|
|
86
|
+
headers: event.headers || {},
|
|
87
|
+
query: event.queryStringParameters || {},
|
|
88
|
+
params: event.pathParameters || {},
|
|
89
|
+
body: event.body ? this.parseBody(event.body, event.isBase64Encoded) : undefined,
|
|
90
|
+
context: {
|
|
91
|
+
requestId: context.awsRequestId,
|
|
92
|
+
functionName: context.functionName,
|
|
93
|
+
remainingTime: context.getRemainingTimeInMillis(),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse request body
|
|
99
|
+
*/
|
|
100
|
+
parseBody(body, isBase64Encoded) {
|
|
101
|
+
try {
|
|
102
|
+
if (isBase64Encoded) {
|
|
103
|
+
const decoded = Buffer.from(body, 'base64').toString('utf-8');
|
|
104
|
+
return JSON.parse(decoded);
|
|
105
|
+
}
|
|
106
|
+
return JSON.parse(body);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Process request through HazelJS router
|
|
114
|
+
*/
|
|
115
|
+
async processRequest(request) {
|
|
116
|
+
if (!this.app) {
|
|
117
|
+
return {
|
|
118
|
+
statusCode: 500,
|
|
119
|
+
body: JSON.stringify({ message: 'Application not initialized' }),
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const router = this.app.getRouter();
|
|
124
|
+
// Build request context for the router
|
|
125
|
+
const context = {
|
|
126
|
+
method: request.method,
|
|
127
|
+
url: request.url,
|
|
128
|
+
headers: request.headers,
|
|
129
|
+
query: request.query,
|
|
130
|
+
params: request.params,
|
|
131
|
+
body: request.body,
|
|
132
|
+
requestId: request.context.requestId,
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const route = await router.match(request.method, request.url, context);
|
|
136
|
+
if (!route) {
|
|
137
|
+
return {
|
|
138
|
+
statusCode: 404,
|
|
139
|
+
body: JSON.stringify({ statusCode: 404, message: 'Route not found' }),
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Create a synthetic request/response to capture the handler output
|
|
144
|
+
const syntheticReq = {
|
|
145
|
+
method: request.method,
|
|
146
|
+
url: request.url,
|
|
147
|
+
headers: request.headers,
|
|
148
|
+
query: request.query,
|
|
149
|
+
params: route.context?.params ?? context.params ?? {},
|
|
150
|
+
body: request.body,
|
|
151
|
+
};
|
|
152
|
+
let responseBody;
|
|
153
|
+
let responseStatus = 200;
|
|
154
|
+
const responseHeaders = { 'Content-Type': 'application/json' };
|
|
155
|
+
const syntheticRes = {
|
|
156
|
+
statusCode: 200,
|
|
157
|
+
status(code) {
|
|
158
|
+
responseStatus = code;
|
|
159
|
+
return syntheticRes;
|
|
160
|
+
},
|
|
161
|
+
json(data) {
|
|
162
|
+
responseBody = data;
|
|
163
|
+
responseStatus = responseStatus || 200;
|
|
164
|
+
},
|
|
165
|
+
send(data) {
|
|
166
|
+
responseBody = data;
|
|
167
|
+
},
|
|
168
|
+
setHeader(key, value) {
|
|
169
|
+
responseHeaders[key] = value;
|
|
170
|
+
},
|
|
171
|
+
getHeader(key) {
|
|
172
|
+
return responseHeaders[key];
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
const result = await route.handler(syntheticReq, syntheticRes, route.context);
|
|
176
|
+
// If handler returned a value directly, use it
|
|
177
|
+
if (result !== undefined && responseBody === undefined) {
|
|
178
|
+
responseBody = result;
|
|
179
|
+
}
|
|
180
|
+
const contentType = responseHeaders['content-type'] ?? responseHeaders['Content-Type'] ?? '';
|
|
181
|
+
const isBinary = this.options.binaryMimeTypes?.length &&
|
|
182
|
+
this.isBinaryContentType(contentType) &&
|
|
183
|
+
(Buffer.isBuffer(responseBody) || responseBody instanceof Uint8Array);
|
|
184
|
+
let body;
|
|
185
|
+
let isBase64Encoded = false;
|
|
186
|
+
if (isBinary) {
|
|
187
|
+
body = Buffer.from(responseBody).toString('base64');
|
|
188
|
+
isBase64Encoded = true;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
body = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
statusCode: responseStatus,
|
|
195
|
+
body,
|
|
196
|
+
headers: responseHeaders,
|
|
197
|
+
...(isBase64Encoded && { isBase64Encoded: true }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const statusCode = error.statusCode || 500;
|
|
202
|
+
const message = error instanceof Error ? error.message : 'Internal Server Error';
|
|
203
|
+
return {
|
|
204
|
+
statusCode,
|
|
205
|
+
body: JSON.stringify({ statusCode, message }),
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Whether the given Content-Type matches any of the binaryMimeTypes patterns.
|
|
212
|
+
*/
|
|
213
|
+
isBinaryContentType(contentType) {
|
|
214
|
+
if (!this.options.binaryMimeTypes?.length || !contentType)
|
|
215
|
+
return false;
|
|
216
|
+
const ct = contentType.split(';')[0].trim().toLowerCase();
|
|
217
|
+
for (const pattern of this.options.binaryMimeTypes) {
|
|
218
|
+
const p = pattern.toLowerCase();
|
|
219
|
+
if (p.endsWith('/*')) {
|
|
220
|
+
if (ct.startsWith(p.slice(0, -1)))
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
else if (ct === p || ct.startsWith(p + '/')) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get application instance
|
|
231
|
+
*/
|
|
232
|
+
getApp() {
|
|
233
|
+
return this.app;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if this is a cold start
|
|
237
|
+
*/
|
|
238
|
+
isCold() {
|
|
239
|
+
return this.isColdStart;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.LambdaAdapter = LambdaAdapter;
|
|
243
|
+
/**
|
|
244
|
+
* Create a Lambda handler for a HazelJS module
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* // handler.ts
|
|
249
|
+
* import { createLambdaHandler } from '@hazeljs/core';
|
|
250
|
+
* import { AppModule } from './app.module';
|
|
251
|
+
*
|
|
252
|
+
* export const handler = createLambdaHandler(AppModule);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
function createLambdaHandler(moduleClass, options) {
|
|
256
|
+
const adapter = new LambdaAdapter(moduleClass, options ?? {});
|
|
257
|
+
return adapter.createHandler();
|
|
258
|
+
}
|