@pikku/lambda 0.12.3 → 0.12.4
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/CHANGELOG.md +11 -0
- package/dist/handler-factories.d.ts +17 -0
- package/dist/handler-factories.js +91 -0
- package/dist/handler-factories.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +3 -0
- package/dist/http/index.js.map +1 -1
- package/dist/http/request-converter-v2.d.ts +2 -0
- package/dist/http/request-converter-v2.js +25 -0
- package/dist/http/request-converter-v2.js.map +1 -0
- package/dist/http/response-converter-v2.d.ts +2 -0
- package/dist/http/response-converter-v2.js +31 -0
- package/dist/http/response-converter-v2.js.map +1 -0
- package/dist/http/run-fetch-v2.d.ts +2 -0
- package/dist/http/run-fetch-v2.js +25 -0
- package/dist/http/run-fetch-v2.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lambda-deployment-service.d.ts +14 -0
- package/dist/lambda-deployment-service.js +89 -0
- package/dist/lambda-deployment-service.js.map +1 -0
- package/dist/scheduled/index.d.ts +1 -0
- package/dist/scheduled/index.js +2 -0
- package/dist/scheduled/index.js.map +1 -0
- package/dist/scheduled/run-scheduled.d.ts +2 -0
- package/dist/scheduled/run-scheduled.js +14 -0
- package/dist/scheduled/run-scheduled.js.map +1 -0
- package/dist/sqs-queue-service.d.ts +10 -0
- package/dist/sqs-queue-service.js +52 -0
- package/dist/sqs-queue-service.js.map +1 -0
- package/dist/websocket/utils.d.ts +1 -1
- package/package.json +10 -5
- package/src/handler-factories.ts +165 -0
- package/src/http/index.ts +3 -0
- package/src/http/request-converter-v2.ts +36 -0
- package/src/http/response-converter-v2.ts +40 -0
- package/src/http/run-fetch-v2.ts +42 -0
- package/src/index.test.ts +13 -0
- package/src/index.ts +4 -0
- package/src/lambda-deployment-service.ts +134 -0
- package/src/scheduled/index.ts +1 -0
- package/src/scheduled/run-scheduled.ts +30 -0
- package/src/sqs-queue-service.ts +91 -0
- package/src/websocket/lambda-eventhub-service.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 0.12.4
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 41ce2cb: Upgrade to TypeScript 6 and raise the minimum Node.js version to 22.
|
|
6
|
+
|
|
7
|
+
All packages now build against `typescript@^6.0.3` and declare `engines.node >= 22`. Internal tooling (`ts-json-schema-generator`, `zod-to-ts`) was bumped to TypeScript 6-compatible releases.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [41ce2cb]
|
|
10
|
+
- @pikku/core@0.12.44
|
|
11
|
+
|
|
1
12
|
## 0.12.0
|
|
2
13
|
|
|
3
14
|
## 0.12.3
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LocalVariablesService } from '@pikku/core/services';
|
|
2
|
+
import type { CoreSingletonServices } from '@pikku/core';
|
|
3
|
+
import type { APIGatewayEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
4
|
+
export interface LambdaServiceFactories {
|
|
5
|
+
createConfig: (variables: LocalVariablesService, ...args: unknown[]) => Promise<unknown>;
|
|
6
|
+
createSingletonServices: (config: unknown, existingServices?: Partial<Record<string, unknown>>) => Promise<CoreSingletonServices>;
|
|
7
|
+
createPlatformServices?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createLambdaHandler(factories: LambdaServiceFactories, handlerTypes: string[]): Record<string, unknown>;
|
|
10
|
+
export declare function createLambdaWorkerHandler(factories: LambdaServiceFactories): {
|
|
11
|
+
handler(event: APIGatewayProxyEventV2): Promise<import("aws-lambda").APIGatewayProxyStructuredResultV2>;
|
|
12
|
+
};
|
|
13
|
+
export declare function createLambdaWebSocketHandler(factories: LambdaServiceFactories): {
|
|
14
|
+
connect(event: APIGatewayEvent): Promise<import("aws-lambda").APIGatewayProxyResult>;
|
|
15
|
+
disconnect(event: APIGatewayEvent): Promise<import("aws-lambda").APIGatewayProxyResult>;
|
|
16
|
+
default(event: APIGatewayEvent): Promise<import("aws-lambda").APIGatewayProxyResult>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services';
|
|
2
|
+
import { runFetchV2 } from './http/run-fetch-v2.js';
|
|
3
|
+
import { runSQSQueueWorker } from './queue/sqs-worker.js';
|
|
4
|
+
import { runLambdaScheduled } from './scheduled/run-scheduled.js';
|
|
5
|
+
let cachedServices = null;
|
|
6
|
+
async function setupServices(factories) {
|
|
7
|
+
if (cachedServices)
|
|
8
|
+
return cachedServices;
|
|
9
|
+
const variables = new LocalVariablesService(process.env);
|
|
10
|
+
const config = await factories.createConfig(variables);
|
|
11
|
+
const secrets = new LocalSecretService(variables);
|
|
12
|
+
const platformServices = factories.createPlatformServices
|
|
13
|
+
? await factories.createPlatformServices()
|
|
14
|
+
: {};
|
|
15
|
+
cachedServices = await factories.createSingletonServices(config, {
|
|
16
|
+
variables,
|
|
17
|
+
secrets,
|
|
18
|
+
...platformServices,
|
|
19
|
+
});
|
|
20
|
+
return cachedServices;
|
|
21
|
+
}
|
|
22
|
+
export function createLambdaHandler(factories, handlerTypes) {
|
|
23
|
+
const result = {};
|
|
24
|
+
if (handlerTypes.includes('fetch')) {
|
|
25
|
+
result.handler = async (event) => {
|
|
26
|
+
await setupServices(factories);
|
|
27
|
+
return runFetchV2(event);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (handlerTypes.includes('queue')) {
|
|
31
|
+
result.queue = async (event) => {
|
|
32
|
+
const services = await setupServices(factories);
|
|
33
|
+
return runSQSQueueWorker(services.logger, event);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (handlerTypes.includes('scheduled')) {
|
|
37
|
+
result.scheduled = async (event) => {
|
|
38
|
+
await setupServices(factories);
|
|
39
|
+
return runLambdaScheduled(event);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!handlerTypes.includes('fetch')) {
|
|
43
|
+
const activeHandlers = handlerTypes.join(', ');
|
|
44
|
+
result.handler = async () => ({
|
|
45
|
+
statusCode: 200,
|
|
46
|
+
body: `Lambda active (handlers: ${activeHandlers})`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
export function createLambdaWorkerHandler(factories) {
|
|
52
|
+
return {
|
|
53
|
+
async handler(event) {
|
|
54
|
+
await setupServices(factories);
|
|
55
|
+
return runFetchV2(event);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function createLambdaWebSocketHandler(factories) {
|
|
60
|
+
const getChannelStore = () => {
|
|
61
|
+
if (!cachedServices) {
|
|
62
|
+
throw new Error('Services not initialized for WebSocket handler');
|
|
63
|
+
}
|
|
64
|
+
const channelStore = cachedServices
|
|
65
|
+
.channelStore;
|
|
66
|
+
if (!channelStore) {
|
|
67
|
+
throw new Error('channelStore not found in singleton services. Ensure it is configured for channel units.');
|
|
68
|
+
}
|
|
69
|
+
return channelStore;
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
async connect(event) {
|
|
73
|
+
await setupServices(factories);
|
|
74
|
+
const { connectWebsocket } = await import('./websocket/index.js');
|
|
75
|
+
return connectWebsocket(event, { channelStore: getChannelStore() });
|
|
76
|
+
},
|
|
77
|
+
async disconnect(event) {
|
|
78
|
+
await setupServices(factories);
|
|
79
|
+
const { disconnectWebsocket } = await import('./websocket/index.js');
|
|
80
|
+
return disconnectWebsocket(event, { channelStore: getChannelStore() });
|
|
81
|
+
},
|
|
82
|
+
async default(event) {
|
|
83
|
+
await setupServices(factories);
|
|
84
|
+
const { processWebsocketMessage } = await import('./websocket/index.js');
|
|
85
|
+
return processWebsocketMessage(event, {
|
|
86
|
+
channelStore: getChannelStore(),
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=handler-factories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-factories.js","sourceRoot":"","sources":["../src/handler-factories.ts"],"names":[],"mappings":"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"}
|
package/dist/http/index.d.ts
CHANGED
package/dist/http/index.js
CHANGED
package/dist/http/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const lambdaV2EventToRequest = (event) => {
|
|
2
|
+
const host = event.headers?.host ?? event.requestContext?.domainName ?? 'localhost';
|
|
3
|
+
const url = new URL(event.rawPath, `https://${host}`);
|
|
4
|
+
if (event.rawQueryString) {
|
|
5
|
+
url.search = `?${event.rawQueryString}`;
|
|
6
|
+
}
|
|
7
|
+
const body = event.body && event.isBase64Encoded
|
|
8
|
+
? Buffer.from(event.body, 'base64')
|
|
9
|
+
: event.body || undefined;
|
|
10
|
+
const method = event.requestContext?.http?.method || 'GET';
|
|
11
|
+
const headers = {};
|
|
12
|
+
if (event.headers) {
|
|
13
|
+
for (const [key, value] of Object.entries(event.headers)) {
|
|
14
|
+
if (value != null) {
|
|
15
|
+
headers[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return new Request(url.toString(), {
|
|
20
|
+
method,
|
|
21
|
+
headers,
|
|
22
|
+
body: ['GET', 'HEAD'].includes(method.toUpperCase()) ? undefined : body,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=request-converter-v2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-converter-v2.js","sourceRoot":"","sources":["../../src/http/request-converter-v2.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,KAA6B,EACpB,EAAE;IACX,MAAM,IAAI,GACR,KAAK,CAAC,OAAO,EAAE,IAAI,IAAI,KAAK,CAAC,cAAc,EAAE,UAAU,IAAI,WAAW,CAAA;IACxE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC,CAAA;IAErD,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAEzB,GAAG,CAAC,MAAM,GAAG,IAAI,KAAK,CAAC,cAAc,EAAE,CAAA;IACzC,CAAC;IAED,MAAM,IAAI,GACR,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,eAAe;QACjC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC;QACnC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,SAAS,CAAA;IAE7B,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,CAAA;IAE1D,MAAM,OAAO,GAA2B,EAAE,CAAA;IAC1C,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACzD,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;QACjC,MAAM;QACN,OAAO;QACP,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;KACxE,CAAC,CAAA;AACJ,CAAC,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const responseToLambdaV2Result = async (response) => {
|
|
2
|
+
const cookies = response.headers.getSetCookie
|
|
3
|
+
? response.headers.getSetCookie()
|
|
4
|
+
: [];
|
|
5
|
+
const headers = {};
|
|
6
|
+
response.headers.forEach((value, key) => {
|
|
7
|
+
if (key.toLowerCase() === 'set-cookie' && cookies.length > 0)
|
|
8
|
+
return;
|
|
9
|
+
headers[key.toLowerCase()] = value;
|
|
10
|
+
});
|
|
11
|
+
let body;
|
|
12
|
+
let isBase64Encoded = false;
|
|
13
|
+
const contentType = response.headers.get('content-type') || '';
|
|
14
|
+
const isBinary = !/^text\/|application\/(json|javascript|xml)/.test(contentType);
|
|
15
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
16
|
+
if (isBinary) {
|
|
17
|
+
body = buffer.toString('base64');
|
|
18
|
+
isBase64Encoded = true;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
body = buffer.toString('utf-8');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
statusCode: response.status,
|
|
25
|
+
headers,
|
|
26
|
+
body,
|
|
27
|
+
isBase64Encoded,
|
|
28
|
+
...(cookies.length > 0 ? { cookies } : {}),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=response-converter-v2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-converter-v2.js","sourceRoot":"","sources":["../../src/http/response-converter-v2.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,EAC3C,QAAkB,EAC0B,EAAE;IAE9C,MAAM,OAAO,GAAa,QAAQ,CAAC,OAAO,CAAC,YAAY;QACrD,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE;QACjC,CAAC,CAAC,EAAE,CAAA;IAEN,MAAM,OAAO,GAA2B,EAAE,CAAA;IAC1C,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtC,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QACpE,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,IAAI,IAAY,CAAA;IAChB,IAAI,eAAe,GAAG,KAAK,CAAA;IAE3B,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAA;IAC9D,MAAM,QAAQ,GAAG,CAAC,4CAA4C,CAAC,IAAI,CACjE,WAAW,CACZ,CAAA;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;IACxD,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;QAChC,eAAe,GAAG,IAAI,CAAA;IACxB,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACjC,CAAC;IAED,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,MAAM;QAC3B,OAAO;QACP,IAAI;QACJ,eAAe;QACf,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3C,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PikkuFetchHTTPResponse, fetchData } from '@pikku/core/http';
|
|
2
|
+
import { responseToLambdaV2Result } from './response-converter-v2.js';
|
|
3
|
+
import { lambdaV2EventToRequest } from './request-converter-v2.js';
|
|
4
|
+
export const runFetchV2 = async (event) => {
|
|
5
|
+
const request = lambdaV2EventToRequest(event);
|
|
6
|
+
const response = new PikkuFetchHTTPResponse();
|
|
7
|
+
if (request.method === 'OPTIONS') {
|
|
8
|
+
response.header('Access-Control-Allow-Origin', request.headers.get('origin') || '*');
|
|
9
|
+
response.header('Access-Control-Allow-Headers', 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent');
|
|
10
|
+
response.header('Access-Control-Allow-Methods', 'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT');
|
|
11
|
+
response.status(200);
|
|
12
|
+
return responseToLambdaV2Result(response.toResponse());
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
await fetchData(request, response);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.error('fetchData error:', err);
|
|
19
|
+
response.status(500);
|
|
20
|
+
response.header('content-type', 'application/json');
|
|
21
|
+
response.json({ error: 'Internal Server Error' });
|
|
22
|
+
}
|
|
23
|
+
return responseToLambdaV2Result(response.toResponse());
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=run-fetch-v2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-fetch-v2.js","sourceRoot":"","sources":["../../src/http/run-fetch-v2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAKpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AAElE,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAA6B,EACe,EAAE;IAC9C,MAAM,OAAO,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAA;IAC7C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;IAE7C,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,QAAQ,CAAC,MAAM,CACb,6BAA6B,EAC7B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CACrC,CAAA;QACD,QAAQ,CAAC,MAAM,CACb,8BAA8B,EAC9B,uFAAuF,CACxF,CAAA;QACD,QAAQ,CAAC,MAAM,CACb,8BAA8B,EAC9B,wCAAwC,CACzC,CAAA;QACD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,OAAO,wBAAwB,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;QACtC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;QACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAA;IACnD,CAAC;IAED,OAAO,wBAAwB,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;AACxD,CAAC,CAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './http/index.js';
|
|
2
2
|
export * from './websocket/index.js';
|
|
3
3
|
export * from './queue/index.js';
|
|
4
|
+
export * from './scheduled/index.js';
|
|
5
|
+
export * from './handler-factories.js';
|
|
6
|
+
export { SQSQueueService } from './sqs-queue-service.js';
|
|
7
|
+
export { LambdaDeploymentService } from './lambda-deployment-service.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export * from './http/index.js';
|
|
2
2
|
export * from './websocket/index.js';
|
|
3
3
|
export * from './queue/index.js';
|
|
4
|
+
export * from './scheduled/index.js';
|
|
5
|
+
export * from './handler-factories.js';
|
|
6
|
+
export { SQSQueueService } from './sqs-queue-service.js';
|
|
7
|
+
export { LambdaDeploymentService } from './lambda-deployment-service.js';
|
|
4
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DeploymentService, DeploymentConfig } from '@pikku/core/services';
|
|
2
|
+
import type { JWTService } from '@pikku/core/services';
|
|
3
|
+
import type { SecretService } from '@pikku/core/services';
|
|
4
|
+
export declare class LambdaDeploymentService implements DeploymentService {
|
|
5
|
+
private jwt;
|
|
6
|
+
private secrets;
|
|
7
|
+
private client;
|
|
8
|
+
private bindings;
|
|
9
|
+
constructor(jwt: JWTService | undefined, secrets: SecretService, functionBindings: Record<string, string>);
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
start(_config: DeploymentConfig): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
invoke(funcName: string, data: unknown, session?: unknown, traceId?: string): Promise<unknown>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
|
|
2
|
+
import { encryptJSON } from '@pikku/core/crypto-utils';
|
|
3
|
+
export class LambdaDeploymentService {
|
|
4
|
+
jwt;
|
|
5
|
+
secrets;
|
|
6
|
+
client;
|
|
7
|
+
bindings;
|
|
8
|
+
constructor(jwt, secrets, functionBindings) {
|
|
9
|
+
this.jwt = jwt;
|
|
10
|
+
this.secrets = secrets;
|
|
11
|
+
this.client = new LambdaClient({});
|
|
12
|
+
this.bindings = new Map(Object.entries(functionBindings));
|
|
13
|
+
}
|
|
14
|
+
async init() { }
|
|
15
|
+
async start(_config) { }
|
|
16
|
+
async stop() { }
|
|
17
|
+
async invoke(funcName, data, session, traceId) {
|
|
18
|
+
const lambdaFunctionName = this.bindings.get(funcName);
|
|
19
|
+
if (!lambdaFunctionName) {
|
|
20
|
+
throw new Error(`No Lambda binding for function '${funcName}'. ` +
|
|
21
|
+
`Available bindings: ${[...this.bindings.keys()].join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
const headers = {
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
...(traceId && { 'x-request-id': traceId }),
|
|
26
|
+
};
|
|
27
|
+
let secret;
|
|
28
|
+
try {
|
|
29
|
+
secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET');
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
if (secret && this.jwt) {
|
|
33
|
+
const sessionEnc = session
|
|
34
|
+
? await encryptJSON(secret, { session })
|
|
35
|
+
: undefined;
|
|
36
|
+
const token = await this.jwt.encode({ value: 5, unit: 'minute' }, {
|
|
37
|
+
aud: 'pikku-remote',
|
|
38
|
+
fn: funcName,
|
|
39
|
+
iat: Math.floor(Date.now() / 1000),
|
|
40
|
+
session: sessionEnc,
|
|
41
|
+
});
|
|
42
|
+
headers.authorization = `Bearer ${token}`;
|
|
43
|
+
}
|
|
44
|
+
const rpcPath = `/remote/rpc/${encodeURIComponent(funcName)}`;
|
|
45
|
+
const apiGatewayEvent = {
|
|
46
|
+
httpMethod: 'POST',
|
|
47
|
+
path: rpcPath,
|
|
48
|
+
headers,
|
|
49
|
+
body: JSON.stringify({ data }),
|
|
50
|
+
isBase64Encoded: false,
|
|
51
|
+
queryStringParameters: null,
|
|
52
|
+
pathParameters: null,
|
|
53
|
+
stageVariables: null,
|
|
54
|
+
requestContext: {
|
|
55
|
+
httpMethod: 'POST',
|
|
56
|
+
path: rpcPath,
|
|
57
|
+
stage: 'prod',
|
|
58
|
+
requestId: crypto.randomUUID(),
|
|
59
|
+
identity: {},
|
|
60
|
+
},
|
|
61
|
+
resource: rpcPath,
|
|
62
|
+
};
|
|
63
|
+
const response = await this.client.send(new InvokeCommand({
|
|
64
|
+
FunctionName: lambdaFunctionName,
|
|
65
|
+
Payload: JSON.stringify(apiGatewayEvent),
|
|
66
|
+
}));
|
|
67
|
+
if (response.FunctionError) {
|
|
68
|
+
const errorPayload = response.Payload
|
|
69
|
+
? new TextDecoder().decode(response.Payload)
|
|
70
|
+
: 'Unknown error';
|
|
71
|
+
throw new Error(`Remote RPC call to '${funcName}' failed: Lambda error: ${errorPayload}`);
|
|
72
|
+
}
|
|
73
|
+
if (!response.Payload) {
|
|
74
|
+
throw new Error(`Remote RPC call to '${funcName}' returned no payload`);
|
|
75
|
+
}
|
|
76
|
+
const lambdaResult = JSON.parse(new TextDecoder().decode(response.Payload));
|
|
77
|
+
if (lambdaResult.statusCode && lambdaResult.statusCode >= 400) {
|
|
78
|
+
throw new Error(`Remote RPC call to '${funcName}' failed: ${lambdaResult.statusCode} ${lambdaResult.body}`);
|
|
79
|
+
}
|
|
80
|
+
const body = lambdaResult.body;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(body);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return body;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=lambda-deployment-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lambda-deployment-service.js","sourceRoot":"","sources":["../src/lambda-deployment-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runLambdaScheduled } from './run-scheduled.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scheduled/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
|
|
2
|
+
export const runLambdaScheduled = async (_event) => {
|
|
3
|
+
const scheduledTasks = getScheduledTasks();
|
|
4
|
+
for (const [name] of scheduledTasks) {
|
|
5
|
+
const traceId = `cron-${crypto.randomUUID()}`;
|
|
6
|
+
try {
|
|
7
|
+
await runScheduledTask({ name, traceId });
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
console.error(`Scheduled task '${name}' (traceId: ${traceId}) failed:`, err);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=run-scheduled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-scheduled.js","sourceRoot":"","sources":["../../src/scheduled/run-scheduled.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,MAAsB,EACP,EAAE;IACjB,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAA;IAI1C,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,cAAc,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,QAAQ,MAAM,CAAC,UAAU,EAAE,EAAE,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CACX,mBAAmB,IAAI,eAAe,OAAO,WAAW,EACxD,GAAG,CACJ,CAAA;QACH,CAAC;IACH,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue';
|
|
2
|
+
export declare class SQSQueueService implements QueueService {
|
|
3
|
+
private queueUrlMap?;
|
|
4
|
+
readonly supportsResults = false;
|
|
5
|
+
private client;
|
|
6
|
+
constructor(queueUrlMap?: Record<string, string> | undefined);
|
|
7
|
+
add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>;
|
|
8
|
+
getJob<T, R>(_queueName: string, _jobId: string): Promise<QueueJob<T, R> | null>;
|
|
9
|
+
private resolveQueueUrl;
|
|
10
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
|
|
2
|
+
export class SQSQueueService {
|
|
3
|
+
queueUrlMap;
|
|
4
|
+
supportsResults = false;
|
|
5
|
+
client;
|
|
6
|
+
constructor(queueUrlMap) {
|
|
7
|
+
this.queueUrlMap = queueUrlMap;
|
|
8
|
+
this.client = new SQSClient({});
|
|
9
|
+
}
|
|
10
|
+
async add(queueName, data, options) {
|
|
11
|
+
const queueUrl = this.resolveQueueUrl(queueName);
|
|
12
|
+
const messageId = crypto.randomUUID();
|
|
13
|
+
const message = {
|
|
14
|
+
id: messageId,
|
|
15
|
+
queueName,
|
|
16
|
+
data,
|
|
17
|
+
pikkuUserId: options?.pikkuUserId,
|
|
18
|
+
};
|
|
19
|
+
const result = await this.client.send(new SendMessageCommand({
|
|
20
|
+
QueueUrl: queueUrl,
|
|
21
|
+
MessageBody: JSON.stringify(message),
|
|
22
|
+
DelaySeconds: options?.delay
|
|
23
|
+
? Math.min(Math.ceil(options.delay / 1000), 900)
|
|
24
|
+
: undefined,
|
|
25
|
+
}));
|
|
26
|
+
return result.MessageId ?? messageId;
|
|
27
|
+
}
|
|
28
|
+
async getJob(_queueName, _jobId) {
|
|
29
|
+
throw new Error('SQSQueueService does not support getJob(). ' +
|
|
30
|
+
'SQS is fire-and-forget. ' +
|
|
31
|
+
'Use BullMQ or PgBoss if you need job result tracking.');
|
|
32
|
+
}
|
|
33
|
+
resolveQueueUrl(queueName) {
|
|
34
|
+
if (this.queueUrlMap?.[queueName]) {
|
|
35
|
+
return this.queueUrlMap[queueName];
|
|
36
|
+
}
|
|
37
|
+
const envKey = `SQS_QUEUE_URL_${toScreamingSnake(queueName)}`;
|
|
38
|
+
const url = process.env[envKey];
|
|
39
|
+
if (!url) {
|
|
40
|
+
throw new Error(`SQS queue URL not found for queue "${queueName}". ` +
|
|
41
|
+
`Set environment variable ${envKey} or pass queueUrlMap to constructor.`);
|
|
42
|
+
}
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function toScreamingSnake(name) {
|
|
47
|
+
return name
|
|
48
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
49
|
+
.replace(/-/g, '_')
|
|
50
|
+
.toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=sqs-queue-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs-queue-service.js","sourceRoot":"","sources":["../src/sqs-queue-service.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAGnE,MAAM,OAAO,eAAe;IAIN;IAHX,eAAe,GAAG,KAAK,CAAA;IACxB,MAAM,CAAW;IAEzB,YAAoB,WAAoC;QAApC,gBAAW,GAAX,WAAW,CAAyB;QACtD,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,GAAG,CACP,SAAiB,EACjB,IAAO,EACP,OAAoB;QAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;QAChD,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QAErC,MAAM,OAAO,GAAG;YACd,EAAE,EAAE,SAAS;YACb,SAAS;YACT,IAAI;YACJ,WAAW,EAAE,OAAO,EAAE,WAAW;SAClC,CAAA;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACnC,IAAI,kBAAkB,CAAC;YACrB,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YACpC,YAAY,EAAE,OAAO,EAAE,KAAK;gBAC1B,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC;gBAChD,CAAC,CAAC,SAAS;SACd,CAAC,CACH,CAAA;QAED,OAAO,MAAM,CAAC,SAAS,IAAI,SAAS,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,MAAM,CACV,UAAkB,EAClB,MAAc;QAEd,MAAM,IAAI,KAAK,CACb,6CAA6C;YAC3C,0BAA0B;YAC1B,uDAAuD,CAC1D,CAAA;IACH,CAAC;IASO,eAAe,CAAC,SAAiB;QACvC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;QACpC,CAAC;QAED,MAAM,MAAM,GAAG,iBAAiB,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAA;QAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAE/B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,sCAAsC,SAAS,KAAK;gBAClD,4BAA4B,MAAM,sCAAsC,CAC3E,CAAA;QACH,CAAC;QAED,OAAO,GAAG,CAAA;IACZ,CAAC;CACF;AAED,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,IAAI;SACR,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;SAClB,WAAW,EAAE,CAAA;AAClB,CAAC"}
|
|
@@ -9,5 +9,5 @@ export declare const getServerlessDependencies: (channelStore: ChannelStore, eve
|
|
|
9
9
|
channelId: string;
|
|
10
10
|
callbackAPI: ApiGatewayManagementApiClient;
|
|
11
11
|
channelHandlerFactory: import("@pikku/core/channel").PikkuChannelHandlerFactory;
|
|
12
|
-
channelStore: ChannelStore<unknown, unknown, import("@pikku/core
|
|
12
|
+
channelStore: ChannelStore<unknown, unknown, import("@pikku/core/channel").Channel<unknown, unknown>>;
|
|
13
13
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/lambda",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -20,19 +20,24 @@
|
|
|
20
20
|
".": "./dist/index.js",
|
|
21
21
|
"./http": "./dist/http/index.js",
|
|
22
22
|
"./websocket": "./dist/websocket/index.js",
|
|
23
|
-
"./queue": "./dist/queue/index.js"
|
|
23
|
+
"./queue": "./dist/queue/index.js",
|
|
24
|
+
"./scheduled": "./dist/scheduled/index.js"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
27
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.713.0",
|
|
27
|
-
"@
|
|
28
|
+
"@aws-sdk/client-lambda": "^3.713.0",
|
|
29
|
+
"@aws-sdk/client-sqs": "^3.713.0",
|
|
30
|
+
"@pikku/core": "^0.12.44",
|
|
28
31
|
"@types/aws-lambda": "^8.10.161"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.1000.0",
|
|
35
|
+
"@aws-sdk/client-lambda": "^3.1000.0",
|
|
36
|
+
"@aws-sdk/client-sqs": "^3.1000.0",
|
|
32
37
|
"@types/aws-lambda": "^8.10.161",
|
|
33
|
-
"typescript": "^
|
|
38
|
+
"typescript": "^6.0.3"
|
|
34
39
|
},
|
|
35
40
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
41
|
+
"node": ">=22"
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda handler factories — mirrors Cloudflare's handler-factories.ts.
|
|
3
|
+
*
|
|
4
|
+
* Creates combined Lambda handlers based on which handler types a unit needs
|
|
5
|
+
* (fetch, queue, scheduled). Services are cached across warm invocations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services'
|
|
9
|
+
import type { CoreSingletonServices } from '@pikku/core'
|
|
10
|
+
import type { ChannelStore } from '@pikku/core/channel'
|
|
11
|
+
import { runFetchV2 } from './http/run-fetch-v2.js'
|
|
12
|
+
import { runSQSQueueWorker } from './queue/sqs-worker.js'
|
|
13
|
+
import { runLambdaScheduled } from './scheduled/run-scheduled.js'
|
|
14
|
+
import type {
|
|
15
|
+
APIGatewayEvent,
|
|
16
|
+
APIGatewayProxyEventV2,
|
|
17
|
+
SQSEvent,
|
|
18
|
+
ScheduledEvent,
|
|
19
|
+
} from 'aws-lambda'
|
|
20
|
+
|
|
21
|
+
export interface LambdaServiceFactories {
|
|
22
|
+
createConfig: (
|
|
23
|
+
variables: LocalVariablesService,
|
|
24
|
+
...args: unknown[]
|
|
25
|
+
) => Promise<unknown>
|
|
26
|
+
createSingletonServices: (
|
|
27
|
+
config: unknown,
|
|
28
|
+
existingServices?: Partial<Record<string, unknown>>
|
|
29
|
+
) => Promise<CoreSingletonServices>
|
|
30
|
+
/**
|
|
31
|
+
* Optional callback to create platform services from environment.
|
|
32
|
+
* Returns services to merge into existingServices.
|
|
33
|
+
* Generated by the deploy codegen based on unit capabilities.
|
|
34
|
+
*/
|
|
35
|
+
createPlatformServices?: () =>
|
|
36
|
+
| Record<string, unknown>
|
|
37
|
+
| Promise<Record<string, unknown>>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let cachedServices: CoreSingletonServices | null = null
|
|
41
|
+
|
|
42
|
+
async function setupServices(
|
|
43
|
+
factories: LambdaServiceFactories
|
|
44
|
+
): Promise<CoreSingletonServices> {
|
|
45
|
+
if (cachedServices) return cachedServices
|
|
46
|
+
const variables = new LocalVariablesService(
|
|
47
|
+
process.env as Record<string, string | undefined>
|
|
48
|
+
)
|
|
49
|
+
const config = await factories.createConfig(variables)
|
|
50
|
+
const secrets = new LocalSecretService(variables)
|
|
51
|
+
|
|
52
|
+
const platformServices = factories.createPlatformServices
|
|
53
|
+
? await factories.createPlatformServices()
|
|
54
|
+
: {}
|
|
55
|
+
|
|
56
|
+
cachedServices = await factories.createSingletonServices(config, {
|
|
57
|
+
variables,
|
|
58
|
+
secrets,
|
|
59
|
+
...platformServices,
|
|
60
|
+
})
|
|
61
|
+
return cachedServices
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a combined Lambda handler based on which handler types the unit needs.
|
|
66
|
+
*
|
|
67
|
+
* Returns an object with named exports that serverless.yml references:
|
|
68
|
+
* - `handler` for HTTP (API Gateway)
|
|
69
|
+
* - `queue` for SQS
|
|
70
|
+
* - `scheduled` for EventBridge
|
|
71
|
+
*/
|
|
72
|
+
export function createLambdaHandler(
|
|
73
|
+
factories: LambdaServiceFactories,
|
|
74
|
+
handlerTypes: string[]
|
|
75
|
+
) {
|
|
76
|
+
const result: Record<string, unknown> = {}
|
|
77
|
+
|
|
78
|
+
if (handlerTypes.includes('fetch')) {
|
|
79
|
+
result.handler = async (event: APIGatewayProxyEventV2) => {
|
|
80
|
+
await setupServices(factories)
|
|
81
|
+
return runFetchV2(event)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (handlerTypes.includes('queue')) {
|
|
86
|
+
result.queue = async (event: SQSEvent) => {
|
|
87
|
+
const services = await setupServices(factories)
|
|
88
|
+
return runSQSQueueWorker(services.logger, event)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (handlerTypes.includes('scheduled')) {
|
|
93
|
+
result.scheduled = async (event: ScheduledEvent) => {
|
|
94
|
+
await setupServices(factories)
|
|
95
|
+
return runLambdaScheduled(event)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If no fetch handler was requested, add a health check
|
|
100
|
+
if (!handlerTypes.includes('fetch')) {
|
|
101
|
+
const activeHandlers = handlerTypes.join(', ')
|
|
102
|
+
result.handler = async () => ({
|
|
103
|
+
statusCode: 200,
|
|
104
|
+
body: `Lambda active (handlers: ${activeHandlers})`,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates a Lambda handler with a fetch() entrypoint for HTTP, agent, RPC,
|
|
113
|
+
* and workflow-orchestrator units.
|
|
114
|
+
*/
|
|
115
|
+
export function createLambdaWorkerHandler(factories: LambdaServiceFactories) {
|
|
116
|
+
return {
|
|
117
|
+
async handler(event: APIGatewayProxyEventV2) {
|
|
118
|
+
await setupServices(factories)
|
|
119
|
+
return runFetchV2(event)
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates a Lambda handler for WebSocket channel units.
|
|
126
|
+
* Exports separate handlers for $connect, $disconnect, and $default routes.
|
|
127
|
+
*/
|
|
128
|
+
export function createLambdaWebSocketHandler(
|
|
129
|
+
factories: LambdaServiceFactories
|
|
130
|
+
) {
|
|
131
|
+
// Import dynamically to avoid pulling in websocket deps for non-channel units
|
|
132
|
+
const getChannelStore = () => {
|
|
133
|
+
if (!cachedServices) {
|
|
134
|
+
throw new Error('Services not initialized for WebSocket handler')
|
|
135
|
+
}
|
|
136
|
+
const channelStore = (cachedServices as unknown as Record<string, unknown>)
|
|
137
|
+
.channelStore as ChannelStore | undefined
|
|
138
|
+
if (!channelStore) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'channelStore not found in singleton services. Ensure it is configured for channel units.'
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return channelStore
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
async connect(event: APIGatewayEvent) {
|
|
148
|
+
await setupServices(factories)
|
|
149
|
+
const { connectWebsocket } = await import('./websocket/index.js')
|
|
150
|
+
return connectWebsocket(event, { channelStore: getChannelStore() })
|
|
151
|
+
},
|
|
152
|
+
async disconnect(event: APIGatewayEvent) {
|
|
153
|
+
await setupServices(factories)
|
|
154
|
+
const { disconnectWebsocket } = await import('./websocket/index.js')
|
|
155
|
+
return disconnectWebsocket(event, { channelStore: getChannelStore() })
|
|
156
|
+
},
|
|
157
|
+
async default(event: APIGatewayEvent) {
|
|
158
|
+
await setupServices(factories)
|
|
159
|
+
const { processWebsocketMessage } = await import('./websocket/index.js')
|
|
160
|
+
return processWebsocketMessage(event, {
|
|
161
|
+
channelStore: getChannelStore(),
|
|
162
|
+
})
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
|
|
2
|
+
|
|
3
|
+
export const lambdaV2EventToRequest = (
|
|
4
|
+
event: APIGatewayProxyEventV2
|
|
5
|
+
): Request => {
|
|
6
|
+
const host =
|
|
7
|
+
event.headers?.host ?? event.requestContext?.domainName ?? 'localhost'
|
|
8
|
+
const url = new URL(event.rawPath, `https://${host}`)
|
|
9
|
+
|
|
10
|
+
if (event.rawQueryString) {
|
|
11
|
+
// rawQueryString is already encoded, append directly
|
|
12
|
+
url.search = `?${event.rawQueryString}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const body =
|
|
16
|
+
event.body && event.isBase64Encoded
|
|
17
|
+
? Buffer.from(event.body, 'base64')
|
|
18
|
+
: event.body || undefined
|
|
19
|
+
|
|
20
|
+
const method = event.requestContext?.http?.method || 'GET'
|
|
21
|
+
|
|
22
|
+
const headers: Record<string, string> = {}
|
|
23
|
+
if (event.headers) {
|
|
24
|
+
for (const [key, value] of Object.entries(event.headers)) {
|
|
25
|
+
if (value != null) {
|
|
26
|
+
headers[key] = value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Request(url.toString(), {
|
|
32
|
+
method,
|
|
33
|
+
headers,
|
|
34
|
+
body: ['GET', 'HEAD'].includes(method.toUpperCase()) ? undefined : body,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
|
|
2
|
+
|
|
3
|
+
export const responseToLambdaV2Result = async (
|
|
4
|
+
response: Response
|
|
5
|
+
): Promise<APIGatewayProxyStructuredResultV2> => {
|
|
6
|
+
// Extract Set-Cookie headers as cookies array (v2 format)
|
|
7
|
+
const cookies: string[] = response.headers.getSetCookie
|
|
8
|
+
? response.headers.getSetCookie()
|
|
9
|
+
: []
|
|
10
|
+
|
|
11
|
+
const headers: Record<string, string> = {}
|
|
12
|
+
response.headers.forEach((value, key) => {
|
|
13
|
+
if (key.toLowerCase() === 'set-cookie' && cookies.length > 0) return
|
|
14
|
+
headers[key.toLowerCase()] = value
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
let body: string
|
|
18
|
+
let isBase64Encoded = false
|
|
19
|
+
|
|
20
|
+
const contentType = response.headers.get('content-type') || ''
|
|
21
|
+
const isBinary = !/^text\/|application\/(json|javascript|xml)/.test(
|
|
22
|
+
contentType
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
26
|
+
if (isBinary) {
|
|
27
|
+
body = buffer.toString('base64')
|
|
28
|
+
isBase64Encoded = true
|
|
29
|
+
} else {
|
|
30
|
+
body = buffer.toString('utf-8')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
statusCode: response.status,
|
|
35
|
+
headers,
|
|
36
|
+
body,
|
|
37
|
+
isBase64Encoded,
|
|
38
|
+
...(cookies.length > 0 ? { cookies } : {}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PikkuFetchHTTPResponse, fetchData } from '@pikku/core/http'
|
|
2
|
+
import type {
|
|
3
|
+
APIGatewayProxyEventV2,
|
|
4
|
+
APIGatewayProxyStructuredResultV2,
|
|
5
|
+
} from 'aws-lambda'
|
|
6
|
+
import { responseToLambdaV2Result } from './response-converter-v2.js'
|
|
7
|
+
import { lambdaV2EventToRequest } from './request-converter-v2.js'
|
|
8
|
+
|
|
9
|
+
export const runFetchV2 = async (
|
|
10
|
+
event: APIGatewayProxyEventV2
|
|
11
|
+
): Promise<APIGatewayProxyStructuredResultV2> => {
|
|
12
|
+
const request = lambdaV2EventToRequest(event)
|
|
13
|
+
const response = new PikkuFetchHTTPResponse()
|
|
14
|
+
|
|
15
|
+
if (request.method === 'OPTIONS') {
|
|
16
|
+
response.header(
|
|
17
|
+
'Access-Control-Allow-Origin',
|
|
18
|
+
request.headers.get('origin') || '*'
|
|
19
|
+
)
|
|
20
|
+
response.header(
|
|
21
|
+
'Access-Control-Allow-Headers',
|
|
22
|
+
'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'
|
|
23
|
+
)
|
|
24
|
+
response.header(
|
|
25
|
+
'Access-Control-Allow-Methods',
|
|
26
|
+
'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'
|
|
27
|
+
)
|
|
28
|
+
response.status(200)
|
|
29
|
+
return responseToLambdaV2Result(response.toResponse())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await fetchData(request, response)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('fetchData error:', err)
|
|
36
|
+
response.status(500)
|
|
37
|
+
response.header('content-type', 'application/json')
|
|
38
|
+
response.json({ error: 'Internal Server Error' })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return responseToLambdaV2Result(response.toResponse())
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import * as lambdaRuntime from './index.js'
|
|
5
|
+
|
|
6
|
+
describe('@pikku/lambda', () => {
|
|
7
|
+
test('exports the lambda runtime API', () => {
|
|
8
|
+
assert.equal(typeof lambdaRuntime.createLambdaHandler, 'function')
|
|
9
|
+
assert.equal(typeof lambdaRuntime.LambdaDeploymentService, 'function')
|
|
10
|
+
assert.equal(typeof lambdaRuntime.SQSQueueService, 'function')
|
|
11
|
+
assert.equal(typeof lambdaRuntime.runFetch, 'function')
|
|
12
|
+
})
|
|
13
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './http/index.js'
|
|
2
2
|
export * from './websocket/index.js'
|
|
3
3
|
export * from './queue/index.js'
|
|
4
|
+
export * from './scheduled/index.js'
|
|
5
|
+
export * from './handler-factories.js'
|
|
6
|
+
export { SQSQueueService } from './sqs-queue-service.js'
|
|
7
|
+
export { LambdaDeploymentService } from './lambda-deployment-service.js'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda deployment service — routes remote() calls via Lambda Invoke.
|
|
3
|
+
*
|
|
4
|
+
* When a gateway (agent, MCP, channel, workflow orchestrator) calls
|
|
5
|
+
* remote('funcName'), this service dispatches to the function's Lambda
|
|
6
|
+
* via the AWS SDK InvokeCommand. Equivalent to Cloudflare's service bindings.
|
|
7
|
+
*
|
|
8
|
+
* Session propagation uses the existing pikkuRemoteAuthMiddleware:
|
|
9
|
+
* JWT signed with PIKKU_REMOTE_SECRET, session encrypted in payload.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'
|
|
13
|
+
import type { DeploymentService, DeploymentConfig } from '@pikku/core/services'
|
|
14
|
+
import type { JWTService } from '@pikku/core/services'
|
|
15
|
+
import type { SecretService } from '@pikku/core/services'
|
|
16
|
+
import { encryptJSON } from '@pikku/core/crypto-utils'
|
|
17
|
+
|
|
18
|
+
export class LambdaDeploymentService implements DeploymentService {
|
|
19
|
+
private client: LambdaClient
|
|
20
|
+
private bindings: Map<string, string>
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private jwt: JWTService | undefined,
|
|
24
|
+
private secrets: SecretService,
|
|
25
|
+
functionBindings: Record<string, string>
|
|
26
|
+
) {
|
|
27
|
+
this.client = new LambdaClient({})
|
|
28
|
+
this.bindings = new Map(Object.entries(functionBindings))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init(): Promise<void> {}
|
|
32
|
+
async start(_config: DeploymentConfig): Promise<void> {}
|
|
33
|
+
async stop(): Promise<void> {}
|
|
34
|
+
|
|
35
|
+
async invoke(
|
|
36
|
+
funcName: string,
|
|
37
|
+
data: unknown,
|
|
38
|
+
session?: unknown,
|
|
39
|
+
traceId?: string
|
|
40
|
+
): Promise<unknown> {
|
|
41
|
+
const lambdaFunctionName = this.bindings.get(funcName)
|
|
42
|
+
if (!lambdaFunctionName) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`No Lambda binding for function '${funcName}'. ` +
|
|
45
|
+
`Available bindings: ${[...this.bindings.keys()].join(', ')}`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const headers: Record<string, string> = {
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
...(traceId && { 'x-request-id': traceId }),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Sign session as JWT for pikkuRemoteAuthMiddleware
|
|
55
|
+
let secret: string | undefined
|
|
56
|
+
try {
|
|
57
|
+
secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET')
|
|
58
|
+
} catch {}
|
|
59
|
+
|
|
60
|
+
if (secret && this.jwt) {
|
|
61
|
+
const sessionEnc = session
|
|
62
|
+
? await encryptJSON(secret, { session })
|
|
63
|
+
: undefined
|
|
64
|
+
const token = await this.jwt.encode(
|
|
65
|
+
{ value: 5, unit: 'minute' },
|
|
66
|
+
{
|
|
67
|
+
aud: 'pikku-remote',
|
|
68
|
+
fn: funcName,
|
|
69
|
+
iat: Math.floor(Date.now() / 1000),
|
|
70
|
+
session: sessionEnc,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
headers.authorization = `Bearer ${token}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build an API Gateway v2 event for the target Lambda
|
|
77
|
+
const rpcPath = `/remote/rpc/${encodeURIComponent(funcName)}`
|
|
78
|
+
const apiGatewayEvent = {
|
|
79
|
+
httpMethod: 'POST',
|
|
80
|
+
path: rpcPath,
|
|
81
|
+
headers,
|
|
82
|
+
body: JSON.stringify({ data }),
|
|
83
|
+
isBase64Encoded: false,
|
|
84
|
+
queryStringParameters: null,
|
|
85
|
+
pathParameters: null,
|
|
86
|
+
stageVariables: null,
|
|
87
|
+
requestContext: {
|
|
88
|
+
httpMethod: 'POST',
|
|
89
|
+
path: rpcPath,
|
|
90
|
+
stage: 'prod',
|
|
91
|
+
requestId: crypto.randomUUID(),
|
|
92
|
+
identity: {},
|
|
93
|
+
},
|
|
94
|
+
resource: rpcPath,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const response = await this.client.send(
|
|
98
|
+
new InvokeCommand({
|
|
99
|
+
FunctionName: lambdaFunctionName,
|
|
100
|
+
Payload: JSON.stringify(apiGatewayEvent),
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (response.FunctionError) {
|
|
105
|
+
const errorPayload = response.Payload
|
|
106
|
+
? new TextDecoder().decode(response.Payload)
|
|
107
|
+
: 'Unknown error'
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Remote RPC call to '${funcName}' failed: Lambda error: ${errorPayload}`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!response.Payload) {
|
|
114
|
+
throw new Error(`Remote RPC call to '${funcName}' returned no payload`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The Lambda returns an API Gateway response shape
|
|
118
|
+
const lambdaResult = JSON.parse(new TextDecoder().decode(response.Payload))
|
|
119
|
+
|
|
120
|
+
if (lambdaResult.statusCode && lambdaResult.statusCode >= 400) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Remote RPC call to '${funcName}' failed: ${lambdaResult.statusCode} ${lambdaResult.body}`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse the body from the API Gateway response
|
|
127
|
+
const body = lambdaResult.body
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(body)
|
|
130
|
+
} catch {
|
|
131
|
+
return body
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runLambdaScheduled } from './run-scheduled.js'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda handler for EventBridge scheduled events.
|
|
3
|
+
*
|
|
4
|
+
* Since each Lambda corresponds to a single unit, and each unit has
|
|
5
|
+
* specific scheduled tasks registered, we run all tasks in this unit
|
|
6
|
+
* when the schedule fires.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ScheduledEvent } from 'aws-lambda'
|
|
10
|
+
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
|
|
11
|
+
|
|
12
|
+
export const runLambdaScheduled = async (
|
|
13
|
+
_event: ScheduledEvent
|
|
14
|
+
): Promise<void> => {
|
|
15
|
+
const scheduledTasks = getScheduledTasks()
|
|
16
|
+
|
|
17
|
+
// Run all scheduled tasks registered in this unit.
|
|
18
|
+
// Typically one task per unit, but iterate all to be safe.
|
|
19
|
+
for (const [name] of scheduledTasks) {
|
|
20
|
+
const traceId = `cron-${crypto.randomUUID()}`
|
|
21
|
+
try {
|
|
22
|
+
await runScheduledTask({ name, traceId })
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(
|
|
25
|
+
`Scheduled task '${name}' (traceId: ${traceId}) failed:`,
|
|
26
|
+
err
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQS implementation of QueueService (producer side).
|
|
3
|
+
*
|
|
4
|
+
* Sends messages to SQS queues. Queue URLs are resolved from environment
|
|
5
|
+
* variables following the convention: SQS_QUEUE_URL_<SCREAMING_SNAKE_NAME>.
|
|
6
|
+
*
|
|
7
|
+
* The consumer side is handled by sqs-worker.ts (runSQSQueueWorker).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'
|
|
11
|
+
import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue'
|
|
12
|
+
|
|
13
|
+
export class SQSQueueService implements QueueService {
|
|
14
|
+
readonly supportsResults = false
|
|
15
|
+
private client: SQSClient
|
|
16
|
+
|
|
17
|
+
constructor(private queueUrlMap?: Record<string, string>) {
|
|
18
|
+
this.client = new SQSClient({})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async add<T>(
|
|
22
|
+
queueName: string,
|
|
23
|
+
data: T,
|
|
24
|
+
options?: JobOptions
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
const queueUrl = this.resolveQueueUrl(queueName)
|
|
27
|
+
const messageId = crypto.randomUUID()
|
|
28
|
+
|
|
29
|
+
const message = {
|
|
30
|
+
id: messageId,
|
|
31
|
+
queueName,
|
|
32
|
+
data,
|
|
33
|
+
pikkuUserId: options?.pikkuUserId,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await this.client.send(
|
|
37
|
+
new SendMessageCommand({
|
|
38
|
+
QueueUrl: queueUrl,
|
|
39
|
+
MessageBody: JSON.stringify(message),
|
|
40
|
+
DelaySeconds: options?.delay
|
|
41
|
+
? Math.min(Math.ceil(options.delay / 1000), 900)
|
|
42
|
+
: undefined,
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return result.MessageId ?? messageId
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getJob<T, R>(
|
|
50
|
+
_queueName: string,
|
|
51
|
+
_jobId: string
|
|
52
|
+
): Promise<QueueJob<T, R> | null> {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'SQSQueueService does not support getJob(). ' +
|
|
55
|
+
'SQS is fire-and-forget. ' +
|
|
56
|
+
'Use BullMQ or PgBoss if you need job result tracking.'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the SQS queue URL for a given queue name.
|
|
62
|
+
*
|
|
63
|
+
* Checks in order:
|
|
64
|
+
* 1. Explicit queueUrlMap passed in constructor
|
|
65
|
+
* 2. Environment variable SQS_QUEUE_URL_<SCREAMING_SNAKE>
|
|
66
|
+
*/
|
|
67
|
+
private resolveQueueUrl(queueName: string): string {
|
|
68
|
+
if (this.queueUrlMap?.[queueName]) {
|
|
69
|
+
return this.queueUrlMap[queueName]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const envKey = `SQS_QUEUE_URL_${toScreamingSnake(queueName)}`
|
|
73
|
+
const url = process.env[envKey]
|
|
74
|
+
|
|
75
|
+
if (!url) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`SQS queue URL not found for queue "${queueName}". ` +
|
|
78
|
+
`Set environment variable ${envKey} or pass queueUrlMap to constructor.`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return url
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toScreamingSnake(name: string): string {
|
|
87
|
+
return name
|
|
88
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
89
|
+
.replace(/-/g, '_')
|
|
90
|
+
.toUpperCase()
|
|
91
|
+
}
|
|
@@ -8,9 +8,9 @@ import { getApiGatewayManagementApiClient, sendMessages } from './utils.js'
|
|
|
8
8
|
import type { Logger } from '@pikku/core/services'
|
|
9
9
|
import type { APIGatewayEvent } from 'aws-lambda'
|
|
10
10
|
|
|
11
|
-
export class LambdaEventHubService<
|
|
12
|
-
|
|
13
|
-
{
|
|
11
|
+
export class LambdaEventHubService<
|
|
12
|
+
EventTopics extends Record<string, any> = {},
|
|
13
|
+
> implements EventHubService<EventTopics> {
|
|
14
14
|
private callbackAPI: ApiGatewayManagementApiClient
|
|
15
15
|
|
|
16
16
|
constructor(
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.ts","./src/request-converter.ts","./src/response-converter.ts","./src/http/index.ts","./src/http/run-fetch.ts","./src/queue/index.ts","./src/queue/sqs-worker.ts","./src/websocket/index.ts","./src/websocket/lambda-channel-handler.ts","./src/websocket/lambda-eventhub-service.ts","./src/websocket/utils.ts","./src/websocket/websocket-connect.ts","./src/websocket/websocket-disconnect.ts","./src/websocket/websocket-message.ts","./src/websocket/websocket-types.ts"],"version":"
|
|
1
|
+
{"root":["./src/handler-factories.ts","./src/index.ts","./src/lambda-deployment-service.ts","./src/request-converter.ts","./src/response-converter.ts","./src/sqs-queue-service.ts","./src/http/index.ts","./src/http/request-converter-v2.ts","./src/http/response-converter-v2.ts","./src/http/run-fetch-v2.ts","./src/http/run-fetch.ts","./src/queue/index.ts","./src/queue/sqs-worker.ts","./src/scheduled/index.ts","./src/scheduled/run-scheduled.ts","./src/websocket/index.ts","./src/websocket/lambda-channel-handler.ts","./src/websocket/lambda-eventhub-service.ts","./src/websocket/utils.ts","./src/websocket/websocket-connect.ts","./src/websocket/websocket-disconnect.ts","./src/websocket/websocket-message.ts","./src/websocket/websocket-types.ts"],"version":"6.0.3"}
|