@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,432 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
jest.mock('@hazeljs/core', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
HazelApp: jest.fn(),
|
|
12
|
+
Container: { getInstance: jest.fn().mockReturnValue({}) },
|
|
13
|
+
Injectable: () => () => undefined,
|
|
14
|
+
HazelModule: () => () => undefined,
|
|
15
|
+
Type: null,
|
|
16
|
+
default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
17
|
+
}));
|
|
18
|
+
const core_1 = require("@hazeljs/core");
|
|
19
|
+
const lambda_adapter_1 = require("./lambda.adapter");
|
|
20
|
+
const cloud_function_adapter_1 = require("./cloud-function.adapter");
|
|
21
|
+
const cold_start_optimizer_1 = require("./cold-start.optimizer");
|
|
22
|
+
const serverless_decorator_1 = require("./serverless.decorator");
|
|
23
|
+
class MockModule {
|
|
24
|
+
}
|
|
25
|
+
function makeEvent(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
httpMethod: 'GET',
|
|
28
|
+
path: '/test',
|
|
29
|
+
headers: {},
|
|
30
|
+
queryStringParameters: null,
|
|
31
|
+
body: null,
|
|
32
|
+
isBase64Encoded: false,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function makeContext(overrides = {}) {
|
|
37
|
+
return {
|
|
38
|
+
awsRequestId: 'req-123',
|
|
39
|
+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:000:function:test',
|
|
40
|
+
functionName: 'test-fn',
|
|
41
|
+
functionVersion: '$LATEST',
|
|
42
|
+
logGroupName: '/aws/lambda/test',
|
|
43
|
+
logStreamName: '2024/01/01/test',
|
|
44
|
+
memoryLimitInMB: '512',
|
|
45
|
+
getRemainingTimeInMillis: () => 30000,
|
|
46
|
+
done: jest.fn(),
|
|
47
|
+
fail: jest.fn(),
|
|
48
|
+
succeed: jest.fn(),
|
|
49
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
describe('LambdaAdapter', () => {
|
|
54
|
+
let mockMatch;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
mockMatch = jest.fn().mockResolvedValue(null);
|
|
58
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
59
|
+
getRouter: jest.fn().mockReturnValue({ match: mockMatch }),
|
|
60
|
+
}));
|
|
61
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
62
|
+
});
|
|
63
|
+
describe('createHandler() – cold start tracking', () => {
|
|
64
|
+
it('isCold() is true before first call', () => {
|
|
65
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
66
|
+
expect(adapter.isCold()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('isCold() becomes false after first handler call', async () => {
|
|
69
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
70
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
71
|
+
expect(adapter.isCold()).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('getApp() is undefined before handler is called', () => {
|
|
74
|
+
expect(new lambda_adapter_1.LambdaAdapter(MockModule).getApp()).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it('getApp() returns HazelApp after first call', async () => {
|
|
77
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
78
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
79
|
+
expect(adapter.getApp()).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('createHandler() – routing', () => {
|
|
83
|
+
it('returns 404 when route is not found', async () => {
|
|
84
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
85
|
+
const result = await adapter.createHandler()(makeEvent({ path: '/missing' }), makeContext());
|
|
86
|
+
expect(result.statusCode).toBe(404);
|
|
87
|
+
expect(JSON.parse(result.body)).toMatchObject({ message: 'Route not found' });
|
|
88
|
+
});
|
|
89
|
+
it('calls matched route handler and returns json response', async () => {
|
|
90
|
+
mockMatch.mockResolvedValue({
|
|
91
|
+
handler: async (_req, res) => res.json({ ok: true }),
|
|
92
|
+
});
|
|
93
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
94
|
+
expect(result.statusCode).toBe(200);
|
|
95
|
+
expect(JSON.parse(result.body)).toEqual({ ok: true });
|
|
96
|
+
});
|
|
97
|
+
it('uses returned value from handler when res methods are not called', async () => {
|
|
98
|
+
mockMatch.mockResolvedValue({ handler: async () => ({ direct: 'return' }) });
|
|
99
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
100
|
+
expect(JSON.parse(result.body)).toEqual({ direct: 'return' });
|
|
101
|
+
});
|
|
102
|
+
it('respects res.status() code', async () => {
|
|
103
|
+
mockMatch.mockResolvedValue({
|
|
104
|
+
handler: async (_req, res) => res.status(201).json({ created: true }),
|
|
105
|
+
});
|
|
106
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST' }), makeContext());
|
|
107
|
+
expect(result.statusCode).toBe(201);
|
|
108
|
+
});
|
|
109
|
+
it('returns string body from res.send()', async () => {
|
|
110
|
+
mockMatch.mockResolvedValue({
|
|
111
|
+
handler: async (_req, res) => res.send('plain text'),
|
|
112
|
+
});
|
|
113
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
114
|
+
expect(result.body).toBe('plain text');
|
|
115
|
+
});
|
|
116
|
+
it('supports res.setHeader()', async () => {
|
|
117
|
+
mockMatch.mockResolvedValue({
|
|
118
|
+
handler: async (_req, res) => {
|
|
119
|
+
res.setHeader('X-Custom', 'value');
|
|
120
|
+
res.json({});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
124
|
+
expect(result.headers?.['X-Custom']).toBe('value');
|
|
125
|
+
});
|
|
126
|
+
it('returns error statusCode from thrown error with statusCode property', async () => {
|
|
127
|
+
const err = Object.assign(new Error('Forbidden'), { statusCode: 403 });
|
|
128
|
+
mockMatch.mockRejectedValue(err);
|
|
129
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
130
|
+
expect(result.statusCode).toBe(403);
|
|
131
|
+
});
|
|
132
|
+
it('returns 500 when route handler throws without statusCode', async () => {
|
|
133
|
+
mockMatch.mockRejectedValue(new Error('Unexpected'));
|
|
134
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
135
|
+
expect(result.statusCode).toBe(500);
|
|
136
|
+
});
|
|
137
|
+
it('returns base64 body and isBase64Encoded when binaryMimeTypes matches and handler sends Buffer', async () => {
|
|
138
|
+
const binaryPayload = Buffer.from('fake-png-bytes');
|
|
139
|
+
mockMatch.mockResolvedValue({
|
|
140
|
+
handler: async (_req, res) => {
|
|
141
|
+
res.setHeader('Content-Type', 'image/png');
|
|
142
|
+
res.send(binaryPayload);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule, {
|
|
146
|
+
binaryMimeTypes: ['image/png', 'image/*'],
|
|
147
|
+
});
|
|
148
|
+
const result = await adapter.createHandler()(makeEvent(), makeContext());
|
|
149
|
+
expect(result.statusCode).toBe(200);
|
|
150
|
+
expect(result.isBase64Encoded).toBe(true);
|
|
151
|
+
expect(Buffer.from(result.body, 'base64').toString()).toBe('fake-png-bytes');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('createHandler() – request conversion', () => {
|
|
155
|
+
it('parses JSON body', async () => {
|
|
156
|
+
let capturedBody;
|
|
157
|
+
mockMatch.mockResolvedValue({
|
|
158
|
+
handler: async (req, res) => {
|
|
159
|
+
capturedBody = req.body;
|
|
160
|
+
res.json({});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: '{"key":"val"}' }), makeContext());
|
|
164
|
+
expect(capturedBody).toEqual({ key: 'val' });
|
|
165
|
+
});
|
|
166
|
+
it('parses base64-encoded JSON body', async () => {
|
|
167
|
+
let capturedBody;
|
|
168
|
+
mockMatch.mockResolvedValue({
|
|
169
|
+
handler: async (req, res) => {
|
|
170
|
+
capturedBody = req.body;
|
|
171
|
+
res.json({});
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
const b64 = Buffer.from(JSON.stringify({ encoded: true })).toString('base64');
|
|
175
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: b64, isBase64Encoded: true }), makeContext());
|
|
176
|
+
expect(capturedBody).toEqual({ encoded: true });
|
|
177
|
+
});
|
|
178
|
+
it('returns raw string body when JSON parse fails', async () => {
|
|
179
|
+
let capturedBody;
|
|
180
|
+
mockMatch.mockResolvedValue({
|
|
181
|
+
handler: async (req, res) => {
|
|
182
|
+
capturedBody = req.body;
|
|
183
|
+
res.json({});
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: 'not-valid-json' }), makeContext());
|
|
187
|
+
expect(capturedBody).toBe('not-valid-json');
|
|
188
|
+
});
|
|
189
|
+
it('passes query string parameters to handler', async () => {
|
|
190
|
+
let capturedQuery;
|
|
191
|
+
mockMatch.mockResolvedValue({
|
|
192
|
+
handler: async (req, res) => {
|
|
193
|
+
capturedQuery = req.query;
|
|
194
|
+
res.json({});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ queryStringParameters: { page: '2', size: '10' } }), makeContext());
|
|
198
|
+
expect(capturedQuery).toEqual({ page: '2', size: '10' });
|
|
199
|
+
});
|
|
200
|
+
it('passes pathParameters to handler', async () => {
|
|
201
|
+
let capturedParams;
|
|
202
|
+
mockMatch.mockResolvedValue({
|
|
203
|
+
handler: async (req, res) => {
|
|
204
|
+
capturedParams = req.params;
|
|
205
|
+
res.json({});
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ pathParameters: { id: '42' } }), makeContext());
|
|
209
|
+
expect(capturedParams).toEqual({ id: '42' });
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('createHandler() – initialization errors', () => {
|
|
213
|
+
it('returns 500 when HazelApp constructor throws', async () => {
|
|
214
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
215
|
+
throw new Error('Module init failed');
|
|
216
|
+
});
|
|
217
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
218
|
+
expect(result.statusCode).toBe(500);
|
|
219
|
+
expect(JSON.parse(result.body).message).toBe('Module init failed');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('createHandler() – app reuse', () => {
|
|
223
|
+
it('creates HazelApp only once across multiple calls', async () => {
|
|
224
|
+
mockMatch.mockResolvedValue({
|
|
225
|
+
handler: async (_r, res) => res.json({}),
|
|
226
|
+
});
|
|
227
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
228
|
+
const handler = adapter.createHandler();
|
|
229
|
+
await handler(makeEvent(), makeContext());
|
|
230
|
+
await handler(makeEvent(), makeContext());
|
|
231
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('cold start optimization', () => {
|
|
235
|
+
it('calls warmUp when module has @Serverless coldStartOptimization:true', async () => {
|
|
236
|
+
let OptimizedModule = class OptimizedModule {
|
|
237
|
+
};
|
|
238
|
+
OptimizedModule = __decorate([
|
|
239
|
+
(0, serverless_decorator_1.Serverless)({ coldStartOptimization: true })
|
|
240
|
+
], OptimizedModule);
|
|
241
|
+
mockMatch.mockResolvedValue({
|
|
242
|
+
handler: async (_r, res) => res.json({}),
|
|
243
|
+
});
|
|
244
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(OptimizedModule);
|
|
245
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
246
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
describe('createLambdaHandler()', () => {
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
jest.clearAllMocks();
|
|
253
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
254
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn().mockResolvedValue(null) }),
|
|
255
|
+
}));
|
|
256
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
257
|
+
});
|
|
258
|
+
it('returns a callable handler that produces a ServerlessResponse', async () => {
|
|
259
|
+
const handler = (0, lambda_adapter_1.createLambdaHandler)(MockModule);
|
|
260
|
+
const result = await handler(makeEvent(), makeContext());
|
|
261
|
+
expect(result).toHaveProperty('statusCode');
|
|
262
|
+
expect(result).toHaveProperty('body');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
// ─── CloudFunctionAdapter ────────────────────────────────────────────────────
|
|
266
|
+
function makeCloudReq(overrides = {}) {
|
|
267
|
+
return {
|
|
268
|
+
method: 'GET',
|
|
269
|
+
url: '/test',
|
|
270
|
+
path: '/test',
|
|
271
|
+
headers: { 'content-type': 'application/json' },
|
|
272
|
+
query: {},
|
|
273
|
+
...overrides,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function makeCloudRes() {
|
|
277
|
+
const captures = {
|
|
278
|
+
headers: {},
|
|
279
|
+
};
|
|
280
|
+
const res = {
|
|
281
|
+
_captures: captures,
|
|
282
|
+
status: jest.fn().mockReturnThis(),
|
|
283
|
+
set: jest.fn().mockReturnThis(),
|
|
284
|
+
send: jest.fn().mockImplementation((b) => {
|
|
285
|
+
captures.body = b;
|
|
286
|
+
}),
|
|
287
|
+
json: jest.fn().mockImplementation((b) => {
|
|
288
|
+
captures.body = b;
|
|
289
|
+
}),
|
|
290
|
+
end: jest.fn(),
|
|
291
|
+
};
|
|
292
|
+
return res;
|
|
293
|
+
}
|
|
294
|
+
describe('CloudFunctionAdapter', () => {
|
|
295
|
+
let mockMatch;
|
|
296
|
+
beforeEach(() => {
|
|
297
|
+
jest.clearAllMocks();
|
|
298
|
+
mockMatch = jest.fn().mockResolvedValue(null);
|
|
299
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
300
|
+
getRouter: jest.fn().mockReturnValue({ match: mockMatch }),
|
|
301
|
+
}));
|
|
302
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
303
|
+
});
|
|
304
|
+
describe('createHttpHandler()', () => {
|
|
305
|
+
it('calls res.status(404) when route not found', async () => {
|
|
306
|
+
const res = makeCloudRes();
|
|
307
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
308
|
+
expect(res.status).toHaveBeenCalledWith(404);
|
|
309
|
+
expect(res.send).toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
it('calls matched route and sends response', async () => {
|
|
312
|
+
mockMatch.mockResolvedValue({
|
|
313
|
+
handler: async (_r, synRes) => synRes.json({ ok: true }),
|
|
314
|
+
});
|
|
315
|
+
const res = makeCloudRes();
|
|
316
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
317
|
+
expect(res.send).toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
it('calls res.status(500).json() on handler init error', async () => {
|
|
320
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
321
|
+
throw new Error('crash');
|
|
322
|
+
});
|
|
323
|
+
const res = makeCloudRes();
|
|
324
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
325
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
326
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Internal Server Error' }));
|
|
327
|
+
});
|
|
328
|
+
it('normalizes string[] headers to first value', async () => {
|
|
329
|
+
let capturedHeaders;
|
|
330
|
+
mockMatch.mockResolvedValue({
|
|
331
|
+
handler: async (req, res) => {
|
|
332
|
+
capturedHeaders = req.headers;
|
|
333
|
+
res.json({});
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
const res = makeCloudRes();
|
|
337
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq({ headers: { 'x-multi': ['alpha', 'beta'] } }), res);
|
|
338
|
+
expect(capturedHeaders?.['x-multi']).toBe('alpha');
|
|
339
|
+
});
|
|
340
|
+
it('normalizes string[] query params to first value', async () => {
|
|
341
|
+
let capturedQuery;
|
|
342
|
+
mockMatch.mockResolvedValue({
|
|
343
|
+
handler: async (req, res) => {
|
|
344
|
+
capturedQuery = req.query;
|
|
345
|
+
res.json({});
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
const res = makeCloudRes();
|
|
349
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq({ query: { tag: ['first', 'second'] } }), res);
|
|
350
|
+
expect(capturedQuery?.tag).toBe('first');
|
|
351
|
+
});
|
|
352
|
+
it('sets headers on response from internal ServerlessResponse headers', async () => {
|
|
353
|
+
mockMatch.mockResolvedValue({
|
|
354
|
+
handler: async (_r, synRes) => {
|
|
355
|
+
synRes.setHeader('X-Trace', 'abc');
|
|
356
|
+
synRes.json({});
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
const res = makeCloudRes();
|
|
360
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
361
|
+
expect(res.set).toHaveBeenCalledWith('X-Trace', 'abc');
|
|
362
|
+
});
|
|
363
|
+
it('isCold() becomes false after first call', async () => {
|
|
364
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
365
|
+
expect(adapter.isCold()).toBe(true);
|
|
366
|
+
const res = makeCloudRes();
|
|
367
|
+
await adapter.createHttpHandler()(makeCloudReq(), res);
|
|
368
|
+
expect(adapter.isCold()).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
it('getApp() returns undefined before first call', () => {
|
|
371
|
+
expect(new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).getApp()).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
it('creates HazelApp only once across multiple calls', async () => {
|
|
374
|
+
mockMatch.mockResolvedValue({
|
|
375
|
+
handler: async (_r, res) => res.json({}),
|
|
376
|
+
});
|
|
377
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
378
|
+
const handler = adapter.createHttpHandler();
|
|
379
|
+
const res1 = makeCloudRes();
|
|
380
|
+
const res2 = makeCloudRes();
|
|
381
|
+
await handler(makeCloudReq(), res1);
|
|
382
|
+
await handler(makeCloudReq(), res2);
|
|
383
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
describe('createEventHandler()', () => {
|
|
387
|
+
it('resolves without error for a valid event', async () => {
|
|
388
|
+
const handler = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createEventHandler();
|
|
389
|
+
await expect(handler({ data: 'payload' }, { eventType: 'pubsub', resource: 'projects/test' })).resolves.toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
it('rethrows errors from initialization', async () => {
|
|
392
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
393
|
+
throw new Error('init failed');
|
|
394
|
+
});
|
|
395
|
+
const handler = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createEventHandler();
|
|
396
|
+
await expect(handler({}, {})).rejects.toThrow('init failed');
|
|
397
|
+
});
|
|
398
|
+
it('isCold() becomes false after first event call', async () => {
|
|
399
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
400
|
+
await adapter.createEventHandler()({}, {});
|
|
401
|
+
expect(adapter.isCold()).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
describe('createCloudFunctionHandler()', () => {
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
jest.clearAllMocks();
|
|
408
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
409
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn().mockResolvedValue(null) }),
|
|
410
|
+
}));
|
|
411
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
412
|
+
});
|
|
413
|
+
it('returns a callable HTTP handler', async () => {
|
|
414
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionHandler)(MockModule);
|
|
415
|
+
const res = makeCloudRes();
|
|
416
|
+
await handler(makeCloudReq(), res);
|
|
417
|
+
expect(res.send).toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
describe('createCloudFunctionEventHandler()', () => {
|
|
421
|
+
beforeEach(() => {
|
|
422
|
+
jest.clearAllMocks();
|
|
423
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
424
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn() }),
|
|
425
|
+
}));
|
|
426
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
427
|
+
});
|
|
428
|
+
it('returns a callable event handler', async () => {
|
|
429
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionEventHandler)(MockModule);
|
|
430
|
+
await expect(handler({ data: 'test' }, {})).resolves.toBeUndefined();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { HazelApp } from '@hazeljs/core';
|
|
2
|
+
import { Type } from '@hazeljs/core';
|
|
3
|
+
/**
|
|
4
|
+
* Google Cloud Function Request
|
|
5
|
+
*/
|
|
6
|
+
export interface CloudFunctionRequest {
|
|
7
|
+
method: string;
|
|
8
|
+
url: string;
|
|
9
|
+
path: string;
|
|
10
|
+
headers: Record<string, string | string[]>;
|
|
11
|
+
query: Record<string, string | string[]>;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
rawBody?: Buffer;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Google Cloud Function Response
|
|
17
|
+
*/
|
|
18
|
+
export interface CloudFunctionResponse {
|
|
19
|
+
status(code: number): CloudFunctionResponse;
|
|
20
|
+
set(field: string, value: string): CloudFunctionResponse;
|
|
21
|
+
send(body: unknown): void;
|
|
22
|
+
json(body: unknown): void;
|
|
23
|
+
end(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Options for createCloudFunctionHandler
|
|
27
|
+
*/
|
|
28
|
+
export interface CloudFunctionHandlerOptions {
|
|
29
|
+
/** Called after the HazelJS app is initialized. */
|
|
30
|
+
onInit?: (app: HazelApp) => Promise<void>;
|
|
31
|
+
/** Called when the handler throws. */
|
|
32
|
+
onError?: (error: unknown) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Cloud Function adapter for HazelJS
|
|
36
|
+
*/
|
|
37
|
+
export declare class CloudFunctionAdapter {
|
|
38
|
+
private moduleClass;
|
|
39
|
+
private options;
|
|
40
|
+
private app?;
|
|
41
|
+
private optimizer;
|
|
42
|
+
private isColdStart;
|
|
43
|
+
constructor(moduleClass: Type<unknown>, options?: CloudFunctionHandlerOptions);
|
|
44
|
+
/**
|
|
45
|
+
* Initialize the HazelJS application
|
|
46
|
+
*/
|
|
47
|
+
private initialize;
|
|
48
|
+
/**
|
|
49
|
+
* Create Cloud Function HTTP handler
|
|
50
|
+
*/
|
|
51
|
+
createHttpHandler(): (req: CloudFunctionRequest, res: CloudFunctionResponse) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Create Cloud Function event handler (for Pub/Sub, Storage, etc.).
|
|
54
|
+
* Stub: initializes the app and logs the event but does not route to user-defined handlers.
|
|
55
|
+
* Implement custom event handling in your module or a separate entrypoint and invoke it from here.
|
|
56
|
+
*/
|
|
57
|
+
createEventHandler(): (event: unknown, context: unknown) => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Convert Cloud Function request to internal format
|
|
60
|
+
*/
|
|
61
|
+
private convertCloudFunctionRequest;
|
|
62
|
+
/**
|
|
63
|
+
* Normalize headers (convert string[] to string)
|
|
64
|
+
*/
|
|
65
|
+
private normalizeHeaders;
|
|
66
|
+
/**
|
|
67
|
+
* Normalize query parameters
|
|
68
|
+
*/
|
|
69
|
+
private normalizeQuery;
|
|
70
|
+
/**
|
|
71
|
+
* Process request through HazelJS router
|
|
72
|
+
*/
|
|
73
|
+
private processRequest;
|
|
74
|
+
/**
|
|
75
|
+
* Get application instance
|
|
76
|
+
*/
|
|
77
|
+
getApp(): HazelApp | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Check if this is a cold start
|
|
80
|
+
*/
|
|
81
|
+
isCold(): boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a Cloud Function HTTP handler for a HazelJS module
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* // index.ts
|
|
89
|
+
* import { createCloudFunctionHandler } from '@hazeljs/core';
|
|
90
|
+
* import { AppModule } from './app.module';
|
|
91
|
+
*
|
|
92
|
+
* export const handler = createCloudFunctionHandler(AppModule);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function createCloudFunctionHandler(moduleClass: Type<unknown>, options?: CloudFunctionHandlerOptions): (req: CloudFunctionRequest, res: CloudFunctionResponse) => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Create a Cloud Function event handler for a HazelJS module
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // index.ts
|
|
102
|
+
* import { createCloudFunctionEventHandler } from '@hazeljs/core';
|
|
103
|
+
* import { AppModule } from './app.module';
|
|
104
|
+
*
|
|
105
|
+
* export const handler = createCloudFunctionEventHandler(AppModule);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function createCloudFunctionEventHandler(moduleClass: Type<unknown>, options?: CloudFunctionHandlerOptions): (event: unknown, context: unknown) => Promise<void>;
|
|
109
|
+
//# sourceMappingURL=cloud-function.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-function.adapter.d.ts","sourceRoot":"","sources":["../src/cloud-function.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAKrC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,CAAC;IAC5C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAAC;IACzD,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,GAAG,IAAI,IAAI,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,mDAAmD;IACnD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,sCAAsC;IACtC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAM7B,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,2BAAgC;IAKnD;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACH,iBAAiB,KACD,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,KAAG,OAAO,CAAC,IAAI,CAAC;IAsCrF;;;;OAIG;IACH,kBAAkB,KACF,OAAO,OAAO,EAAE,SAAS,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC;IA0BhE;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAoBnC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;YACW,cAAc;IA+G5B;;OAEG;IACH,MAAM,IAAI,QAAQ,GAAG,SAAS;IAI9B;;OAEG;IACH,MAAM,IAAI,OAAO;CAGlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,EAC1B,OAAO,CAAC,EAAE,2BAA2B,GACpC,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAG1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,EAC1B,OAAO,CAAC,EAAE,2BAA2B,GACpC,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAGrD"}
|