@mettlecast/domain-cdk-packer 0.2.91 → 0.2.93
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/dist/DomainStack.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as iam from 'aws-cdk-lib/aws-iam';
|
|
|
9
9
|
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
|
|
10
10
|
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
11
11
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
12
|
-
import { createGroupedLambdas } from './grouped-lambda-factory.js';
|
|
12
|
+
import { createGroupedLambdas, createGroupedApiActionLambda } from './grouped-lambda-factory.js';
|
|
13
13
|
import { IamPolicyBuilder } from './iam/iam-policy-builder.js';
|
|
14
14
|
import { HealthConstruct } from './constructs/health-construct.js';
|
|
15
15
|
import { DashboardConstruct } from './constructs/dashboard-construct.js';
|
|
@@ -124,7 +124,7 @@ export class DomainStack extends cdk.Stack {
|
|
|
124
124
|
const resolvedUserPoolId = userPoolId ?? cdk.Fn.select(1, cdk.Fn.split('/', userPoolArn));
|
|
125
125
|
jwtAuthorizer = new apigwv2.HttpAuthorizer(this, 'CognitoAuthorizer', {
|
|
126
126
|
httpApi: this.httpApi,
|
|
127
|
-
authorizerName:
|
|
127
|
+
authorizerName: `CognitoAuthorizer-${domainId}`,
|
|
128
128
|
type: apigwv2.HttpAuthorizerType.JWT,
|
|
129
129
|
identitySource: ['$request.header.Authorization'],
|
|
130
130
|
jwtAudience: [userPoolClientId],
|
|
@@ -207,17 +207,77 @@ export class DomainStack extends cdk.Stack {
|
|
|
207
207
|
const lambdas = [];
|
|
208
208
|
const byId = new Map();
|
|
209
209
|
for (const group of splitByOutboundAccess(entries)) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
if (primitiveType === 'action') {
|
|
211
|
+
// Split API-exposed actions from internal actions so they can be
|
|
212
|
+
// grouped into a single Lambda each (instead of one Lambda per action).
|
|
213
|
+
const apiExposed = group.entries.filter(e => e.adapter === 'createExposedActionApiHandler' && e.apiMethod && e.apiRoutePath);
|
|
214
|
+
const internal = group.entries.filter(e => !(e.adapter === 'createExposedActionApiHandler' && e.apiMethod && e.apiRoutePath));
|
|
215
|
+
// API-exposed actions → ONE grouped Lambda with route-key dispatch
|
|
216
|
+
if (apiExposed.length > 0) {
|
|
217
|
+
const apiEntries = apiExposed.map(e => ({
|
|
218
|
+
id: e.id,
|
|
219
|
+
handlerFile: e.handlerFile,
|
|
220
|
+
method: e.apiMethod,
|
|
221
|
+
routePath: e.apiRoutePath,
|
|
222
|
+
}));
|
|
223
|
+
const apiLambda = createGroupedApiActionLambda(this, {
|
|
224
|
+
domainId,
|
|
225
|
+
domainRoot: registry.domainRoot,
|
|
226
|
+
apiEntries,
|
|
227
|
+
environment,
|
|
228
|
+
eventBusArn,
|
|
229
|
+
reservedConcurrency,
|
|
230
|
+
logRetentionDays: logRetentionDays ?? 30,
|
|
231
|
+
lambdaGroupId: group.outboundAccess,
|
|
232
|
+
...(vpc ? {
|
|
233
|
+
vpc,
|
|
234
|
+
securityGroups: lambdaSg ? [lambdaSg] : undefined,
|
|
235
|
+
subnetSelection: resolveSubnetSelection(group.outboundAccess),
|
|
236
|
+
} : {}),
|
|
237
|
+
});
|
|
238
|
+
lambdas.push(apiLambda);
|
|
239
|
+
apiExposed.forEach(entry => byId.set(entry.id, apiLambda));
|
|
240
|
+
}
|
|
241
|
+
// Internal actions → existing grouped mode (one Lambda for all)
|
|
242
|
+
if (internal.length > 0) {
|
|
243
|
+
const internalDedicated = internal.some(entry => entry.deployment?.isolation === 'dedicated');
|
|
244
|
+
const internalLambdas = createGroupedLambdas(this, {
|
|
245
|
+
domainId,
|
|
246
|
+
domainRoot: registry.domainRoot,
|
|
247
|
+
primitiveType,
|
|
248
|
+
handlerEntries: internal.map(entry => {
|
|
249
|
+
const handlerEntry = {
|
|
250
|
+
id: entry.id,
|
|
251
|
+
handlerFile: entry.handlerFile,
|
|
252
|
+
};
|
|
253
|
+
if (entry.adapter)
|
|
254
|
+
handlerEntry.adapter = entry.adapter;
|
|
255
|
+
return handlerEntry;
|
|
256
|
+
}),
|
|
257
|
+
environment,
|
|
258
|
+
eventBusArn,
|
|
259
|
+
dedicated: internalDedicated,
|
|
260
|
+
reservedConcurrency,
|
|
261
|
+
logRetentionDays: logRetentionDays ?? 30,
|
|
262
|
+
lambdaGroupId: group.outboundAccess,
|
|
263
|
+
...(vpc ? {
|
|
264
|
+
vpc,
|
|
265
|
+
securityGroups: lambdaSg ? [lambdaSg] : undefined,
|
|
266
|
+
subnetSelection: resolveSubnetSelection(group.outboundAccess),
|
|
267
|
+
} : {}),
|
|
268
|
+
});
|
|
269
|
+
lambdas.push(...internalLambdas);
|
|
270
|
+
if (internalDedicated) {
|
|
271
|
+
internal.forEach((entry, index) => byId.set(entry.id, internalLambdas[index]));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
internal.forEach(entry => byId.set(entry.id, internalLambdas[0]));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Non-action primitives: existing logic
|
|
280
|
+
const dedicated = group.entries.some(entry => entry.deployment?.isolation === 'dedicated');
|
|
221
281
|
const groupLambdas = createGroupedLambdas(this, {
|
|
222
282
|
domainId,
|
|
223
283
|
domainRoot: registry.domainRoot,
|
|
@@ -495,6 +555,8 @@ export class DomainStack extends cdk.Stack {
|
|
|
495
555
|
entry.deployment = action.deployment;
|
|
496
556
|
if (action.exposure?.type === 'api') {
|
|
497
557
|
entry.adapter = 'createExposedActionApiHandler';
|
|
558
|
+
entry.apiMethod = action.exposure.method;
|
|
559
|
+
entry.apiRoutePath = `/${domainId}${action.exposure.path}`;
|
|
498
560
|
}
|
|
499
561
|
return entry;
|
|
500
562
|
});
|
|
@@ -69,17 +69,17 @@ describe('DomainStack', () => {
|
|
|
69
69
|
const eventBusArn = 'arn:aws:events:eu-north-1:123456789012:event-bus/tib-event-bus';
|
|
70
70
|
it('synthesises without error for minimal registry', () => {
|
|
71
71
|
const app = new cdk.App();
|
|
72
|
-
expect(() => new DomainStack(app, 'TestDomainStack', { registry: minimalRegistry, eventBusArn })).not.toThrow();
|
|
72
|
+
expect(() => new DomainStack(app, 'TestDomainStack', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' })).not.toThrow();
|
|
73
73
|
});
|
|
74
74
|
it('template contains HttpApi', () => {
|
|
75
75
|
const app = new cdk.App();
|
|
76
|
-
const stack = new DomainStack(app, 'TestDomainStack2', { registry: minimalRegistry, eventBusArn });
|
|
76
|
+
const stack = new DomainStack(app, 'TestDomainStack2', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' });
|
|
77
77
|
const template = Template.fromStack(stack);
|
|
78
78
|
template.resourceCountIs('AWS::ApiGatewayV2::Api', 1);
|
|
79
79
|
});
|
|
80
80
|
it('template contains one Lambda function for the api entry', () => {
|
|
81
81
|
const app = new cdk.App();
|
|
82
|
-
const stack = new DomainStack(app, 'TestDomainStack3', { registry: minimalRegistry, eventBusArn });
|
|
82
|
+
const stack = new DomainStack(app, 'TestDomainStack3', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' });
|
|
83
83
|
const template = Template.fromStack(stack);
|
|
84
84
|
template.resourceCountIs('AWS::Lambda::Function', 4);
|
|
85
85
|
});
|
|
@@ -90,6 +90,8 @@ describe('DomainStack', () => {
|
|
|
90
90
|
const stack = new DomainStack(app, 'TestDomainStackJwt', {
|
|
91
91
|
registry: minimalRegistry,
|
|
92
92
|
eventBusArn,
|
|
93
|
+
projectId: 'Test',
|
|
94
|
+
envCode: 'Dev',
|
|
93
95
|
userPoolArn,
|
|
94
96
|
userPoolClientId,
|
|
95
97
|
});
|
|
@@ -98,6 +100,21 @@ describe('DomainStack', () => {
|
|
|
98
100
|
AuthorizerType: 'JWT',
|
|
99
101
|
});
|
|
100
102
|
});
|
|
103
|
+
it('authorizer name is unique per-domain (includes domainId)', () => {
|
|
104
|
+
const app = new cdk.App();
|
|
105
|
+
const stack = new DomainStack(app, 'TestDomainStackName', {
|
|
106
|
+
registry: minimalRegistry,
|
|
107
|
+
eventBusArn,
|
|
108
|
+
projectId: 'Test',
|
|
109
|
+
envCode: 'Dev',
|
|
110
|
+
userPoolId: 'eu-north-1_test',
|
|
111
|
+
userPoolClientId: 'client-id',
|
|
112
|
+
});
|
|
113
|
+
const template = Template.fromStack(stack);
|
|
114
|
+
template.hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
|
|
115
|
+
Name: 'CognitoAuthorizer-test-domain',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
101
118
|
it('no authorizer attached when no JWT-protected actions are declared', () => {
|
|
102
119
|
const app = new cdk.App();
|
|
103
120
|
const registryNoAuth = {
|
|
@@ -120,6 +137,8 @@ describe('DomainStack', () => {
|
|
|
120
137
|
const stack = new DomainStack(app, 'TestDomainStackNoAuth', {
|
|
121
138
|
registry: registryNoAuth,
|
|
122
139
|
eventBusArn,
|
|
140
|
+
projectId: 'Test',
|
|
141
|
+
envCode: 'Dev',
|
|
123
142
|
});
|
|
124
143
|
const template = Template.fromStack(stack);
|
|
125
144
|
template.resourceCountIs('AWS::ApiGatewayV2::Authorizer', 0);
|
|
@@ -130,6 +149,8 @@ describe('DomainStack', () => {
|
|
|
130
149
|
const stack = new DomainStack(app, 'TestDomainStackDbSecret', {
|
|
131
150
|
registry: minimalRegistry,
|
|
132
151
|
eventBusArn,
|
|
152
|
+
projectId: 'Test',
|
|
153
|
+
envCode: 'Dev',
|
|
133
154
|
dbSecretArn,
|
|
134
155
|
});
|
|
135
156
|
const template = Template.fromStack(stack);
|
|
@@ -143,6 +164,8 @@ describe('DomainStack', () => {
|
|
|
143
164
|
const stack = new DomainStack(app, 'TestDomainStackSecretGrant', {
|
|
144
165
|
registry: minimalRegistry,
|
|
145
166
|
eventBusArn,
|
|
167
|
+
projectId: 'Test',
|
|
168
|
+
envCode: 'Dev',
|
|
146
169
|
dbSecretArn,
|
|
147
170
|
});
|
|
148
171
|
const template = Template.fromStack(stack);
|
|
@@ -152,7 +175,7 @@ describe('DomainStack', () => {
|
|
|
152
175
|
});
|
|
153
176
|
describe('domain S3 bucket', () => {
|
|
154
177
|
const app = new cdk.App();
|
|
155
|
-
const stack = new DomainStack(app, 'TestDomainStackS3', { registry: minimalRegistry, eventBusArn });
|
|
178
|
+
const stack = new DomainStack(app, 'TestDomainStackS3', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' });
|
|
156
179
|
const template = Template.fromStack(stack);
|
|
157
180
|
it('creates exactly one S3 bucket', () => {
|
|
158
181
|
template.resourceCountIs('AWS::S3::Bucket', 1);
|
|
@@ -170,7 +193,7 @@ describe('DomainStack', () => {
|
|
|
170
193
|
});
|
|
171
194
|
describe('tenant-scoped IAM role (defence in depth)', () => {
|
|
172
195
|
const app = new cdk.App();
|
|
173
|
-
const stack = new DomainStack(app, 'TestDomainStackTenant', { registry: minimalRegistry, eventBusArn });
|
|
196
|
+
const stack = new DomainStack(app, 'TestDomainStackTenant', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' });
|
|
174
197
|
const template = Template.fromStack(stack);
|
|
175
198
|
it('creates a tenant-scoped IAM role', () => {
|
|
176
199
|
template.hasResourceProperties('AWS::IAM::Role', {
|
|
@@ -280,6 +303,8 @@ describe('DomainStack', () => {
|
|
|
280
303
|
const stack = new DomainStack(app, 'TestDomainStackMethods', {
|
|
281
304
|
registry: multiMethodRegistry,
|
|
282
305
|
eventBusArn,
|
|
306
|
+
projectId: 'Test',
|
|
307
|
+
envCode: 'Dev',
|
|
283
308
|
userPoolArn,
|
|
284
309
|
userPoolClientId,
|
|
285
310
|
});
|
|
@@ -302,6 +327,8 @@ describe('DomainStack', () => {
|
|
|
302
327
|
const stack = new DomainStack(app, 'TestDomainStackRouteAuth', {
|
|
303
328
|
registry: multiMethodRegistry,
|
|
304
329
|
eventBusArn,
|
|
330
|
+
projectId: 'Test',
|
|
331
|
+
envCode: 'Dev',
|
|
305
332
|
userPoolArn,
|
|
306
333
|
userPoolClientId,
|
|
307
334
|
});
|
|
@@ -325,6 +352,8 @@ describe('DomainStack', () => {
|
|
|
325
352
|
const stack = new DomainStack(app, 'TestDomainStackUnauthRoute', {
|
|
326
353
|
registry: multiMethodRegistry,
|
|
327
354
|
eventBusArn,
|
|
355
|
+
projectId: 'Test',
|
|
356
|
+
envCode: 'Dev',
|
|
328
357
|
userPoolArn,
|
|
329
358
|
userPoolClientId,
|
|
330
359
|
});
|
|
@@ -340,6 +369,8 @@ describe('DomainStack', () => {
|
|
|
340
369
|
const stack = new DomainStack(app, 'TestDomainStackMissingPool', {
|
|
341
370
|
registry: multiMethodRegistry,
|
|
342
371
|
eventBusArn,
|
|
372
|
+
projectId: 'Test',
|
|
373
|
+
envCode: 'Dev',
|
|
343
374
|
// No userPoolArn / userPoolId / userPoolClientId provided
|
|
344
375
|
});
|
|
345
376
|
// `Annotations.of(stack).errors` is not exposed; read the construct's
|
|
@@ -372,6 +403,8 @@ describe('DomainStack', () => {
|
|
|
372
403
|
const stack = new DomainStack(app, 'TestDomainStackNoJwtNeeded', {
|
|
373
404
|
registry: noJwtRegistry,
|
|
374
405
|
eventBusArn,
|
|
406
|
+
projectId: 'Test',
|
|
407
|
+
envCode: 'Dev',
|
|
375
408
|
});
|
|
376
409
|
const metadata = stack.node.metadata;
|
|
377
410
|
const errorMessages = metadata
|
|
@@ -445,6 +478,8 @@ describe('DomainStack', () => {
|
|
|
445
478
|
const stack = new DomainStack(app, 'TestDomainStackActionRoutes', {
|
|
446
479
|
registry: registryWithActions,
|
|
447
480
|
eventBusArn,
|
|
481
|
+
projectId: 'Test',
|
|
482
|
+
envCode: 'Dev',
|
|
448
483
|
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
449
484
|
userPoolClientId: 'test-client-id',
|
|
450
485
|
});
|
|
@@ -459,6 +494,8 @@ describe('DomainStack', () => {
|
|
|
459
494
|
const stack = new DomainStack(app, 'TestDomainStackInternalOnly', {
|
|
460
495
|
registry: registryWithActions,
|
|
461
496
|
eventBusArn,
|
|
497
|
+
projectId: 'Test',
|
|
498
|
+
envCode: 'Dev',
|
|
462
499
|
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
463
500
|
userPoolClientId: 'test-client-id',
|
|
464
501
|
});
|
|
@@ -472,6 +509,8 @@ describe('DomainStack', () => {
|
|
|
472
509
|
const stack = new DomainStack(app, 'TestDomainStackActionJwt', {
|
|
473
510
|
registry: registryWithActions,
|
|
474
511
|
eventBusArn,
|
|
512
|
+
projectId: 'Test',
|
|
513
|
+
envCode: 'Dev',
|
|
475
514
|
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
476
515
|
userPoolClientId: 'test-client-id',
|
|
477
516
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildDedicatedEntryContent, ADAPTER_BY_PRIMITIVE, EXPOSED_ACTION_ADAPTER, } from '../grouped-lambda-factory.js';
|
|
2
|
+
import { buildDedicatedEntryContent, buildGroupedApiActionEntryContent, ADAPTER_BY_PRIMITIVE, EXPOSED_ACTION_ADAPTER, } from '../grouped-lambda-factory.js';
|
|
3
3
|
/**
|
|
4
4
|
* Wave 7 Task 7.3 (#4619) — narrow, pure tests for the dedicated-mode
|
|
5
5
|
* Lambda entry-content generator.
|
|
@@ -143,4 +143,25 @@ describe('buildDedicatedEntryContent (Wave 7 Task 7.3, #4619)', () => {
|
|
|
143
143
|
expect(EXPOSED_ACTION_ADAPTER).toBe('createExposedActionApiHandler');
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
|
+
describe('buildGroupedApiActionEntryContent', () => {
|
|
147
|
+
it('generates a single entry that imports all API actions and dispatches by routeKey', () => {
|
|
148
|
+
const entries = [
|
|
149
|
+
{ id: 'whoami', handlerFile: 'actions/whoami.ts', method: 'GET', routePath: '/auth/v1/auth/whoami' },
|
|
150
|
+
{ id: 'register-sys-admin', handlerFile: 'actions/register-sys-admin.ts', method: 'POST', routePath: '/auth/v1/auth/register-sys-admin' },
|
|
151
|
+
];
|
|
152
|
+
const content = buildGroupedApiActionEntryContent('auth', entries, '/tmp/entry', '/repo/domains/auth');
|
|
153
|
+
// Imports createExposedActionApiHandler from the runtime
|
|
154
|
+
expect(content).toContain("import { createExposedActionApiHandler } from '@mettlecast/domain-runtime'");
|
|
155
|
+
// Imports each action
|
|
156
|
+
expect(content).toContain('import { whoami } from');
|
|
157
|
+
expect(content).toContain('import { registerSysAdmin } from');
|
|
158
|
+
// Route handlers keyed by routeKey
|
|
159
|
+
expect(content).toContain('"GET /auth/v1/auth/whoami"');
|
|
160
|
+
expect(content).toContain('"POST /auth/v1/auth/register-sys-admin"');
|
|
161
|
+
// Dispatcher reads routeKey from event
|
|
162
|
+
expect(content).toContain('event.requestContext?.routeKey');
|
|
163
|
+
// 404 fallback
|
|
164
|
+
expect(content).toContain('statusCode: 404');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
146
167
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
2
3
|
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
3
4
|
import { Construct } from 'constructs';
|
|
4
5
|
/** Primitive types that can be grouped into a single Lambda. */
|
|
@@ -115,3 +116,32 @@ export declare function buildDedicatedEntryContent(handlerImportPath: string, en
|
|
|
115
116
|
* Grouped mode retains Code.fromAsset for backward compatibility.
|
|
116
117
|
*/
|
|
117
118
|
export declare function createGroupedLambdas(scope: Construct, props: GroupedLambdaProps): lambda.Function[];
|
|
119
|
+
/** Entry for a single API-exposed action within a grouped Lambda. */
|
|
120
|
+
export interface ApiActionRouteEntry {
|
|
121
|
+
id: string;
|
|
122
|
+
handlerFile: string;
|
|
123
|
+
method: string;
|
|
124
|
+
/** Full HTTP route path including the domain prefix (e.g. /auth/v1/auth/whoami). */
|
|
125
|
+
routePath: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Pure entry-content generator for the grouped API-action Lambda.
|
|
129
|
+
*
|
|
130
|
+
* Generates a single TypeScript entry that imports all API-exposed actions,
|
|
131
|
+
* pre-creates per-action HTTP handlers via `createExposedActionApiHandler`,
|
|
132
|
+
* and dispatches by `event.requestContext.routeKey`.
|
|
133
|
+
*
|
|
134
|
+
* Exported so unit tests can assert on the generated content without
|
|
135
|
+
* instantiating CDK or esbuild.
|
|
136
|
+
*/
|
|
137
|
+
export declare function buildGroupedApiActionEntryContent(domainId: string, entries: ApiActionRouteEntry[], entryDir: string, domainRoot: string): string;
|
|
138
|
+
/**
|
|
139
|
+
* Creates a single grouped Lambda for all API-exposed actions in a domain.
|
|
140
|
+
*
|
|
141
|
+
* Uses NodejsFunction (esbuild on-the-fly) with a generated multi-action entry
|
|
142
|
+
* that dispatches by `event.requestContext.routeKey`. All HTTP API routes for
|
|
143
|
+
* the domain's API actions point to this one Lambda.
|
|
144
|
+
*/
|
|
145
|
+
export declare function createGroupedApiActionLambda(scope: Construct, props: Omit<GroupedLambdaProps, 'handlerEntries' | 'primitiveType' | 'dedicated'> & {
|
|
146
|
+
apiEntries: ApiActionRouteEntry[];
|
|
147
|
+
}): lambdaNode.NodejsFunction;
|
|
@@ -226,3 +226,96 @@ function toLogRetention(days) {
|
|
|
226
226
|
};
|
|
227
227
|
return map[days] ?? logs.RetentionDays.ONE_MONTH;
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Pure entry-content generator for the grouped API-action Lambda.
|
|
231
|
+
*
|
|
232
|
+
* Generates a single TypeScript entry that imports all API-exposed actions,
|
|
233
|
+
* pre-creates per-action HTTP handlers via `createExposedActionApiHandler`,
|
|
234
|
+
* and dispatches by `event.requestContext.routeKey`.
|
|
235
|
+
*
|
|
236
|
+
* Exported so unit tests can assert on the generated content without
|
|
237
|
+
* instantiating CDK or esbuild.
|
|
238
|
+
*/
|
|
239
|
+
export function buildGroupedApiActionEntryContent(domainId, entries, entryDir, domainRoot) {
|
|
240
|
+
const lines = [
|
|
241
|
+
`import { createExposedActionApiHandler } from '@mettlecast/domain-runtime';`,
|
|
242
|
+
];
|
|
243
|
+
// Import each action handler
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
const exportName = camelCase(entry.id);
|
|
246
|
+
const absPath = join(domainRoot, entry.handlerFile);
|
|
247
|
+
let rel = relative(entryDir, absPath).replace(/\\/g, '/');
|
|
248
|
+
if (!rel.startsWith('.'))
|
|
249
|
+
rel = `./${rel}`;
|
|
250
|
+
lines.push(`import { ${exportName} } from '${rel}';`);
|
|
251
|
+
}
|
|
252
|
+
lines.push('');
|
|
253
|
+
// Build the action registry (for ctx.actions cross-domain calls)
|
|
254
|
+
const registryEntries = entries
|
|
255
|
+
.map(e => `${JSON.stringify(e.id)}: ${camelCase(e.id)}`)
|
|
256
|
+
.join(', ');
|
|
257
|
+
lines.push(`const domainId = ${JSON.stringify(domainId)};`);
|
|
258
|
+
lines.push(`const actionRegistry = { [domainId]: { ${registryEntries} } };`);
|
|
259
|
+
lines.push('');
|
|
260
|
+
// Pre-create per-route handlers
|
|
261
|
+
lines.push('const routeHandlers: Record<string, (event: any) => Promise<any>> = {');
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
const exportName = camelCase(entry.id);
|
|
264
|
+
const routeKey = `${entry.method} ${entry.routePath}`;
|
|
265
|
+
lines.push(` ${JSON.stringify(routeKey)}: createExposedActionApiHandler(${exportName}, { definingDomain: domainId, domainId, actionRegistry, callerDomainId: domainId }),`);
|
|
266
|
+
}
|
|
267
|
+
lines.push('};');
|
|
268
|
+
lines.push('');
|
|
269
|
+
// Dispatcher
|
|
270
|
+
lines.push('export const handler = async (event: any) => {', ' const routeKey = event.requestContext?.routeKey;', ' const matched = routeHandlers[routeKey];', ' if (!matched) {', ' return { statusCode: 404, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: "Not Found" }) };', ' }', ' return matched(event);', '};');
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Creates a single grouped Lambda for all API-exposed actions in a domain.
|
|
275
|
+
*
|
|
276
|
+
* Uses NodejsFunction (esbuild on-the-fly) with a generated multi-action entry
|
|
277
|
+
* that dispatches by `event.requestContext.routeKey`. All HTTP API routes for
|
|
278
|
+
* the domain's API actions point to this one Lambda.
|
|
279
|
+
*/
|
|
280
|
+
export function createGroupedApiActionLambda(scope, props) {
|
|
281
|
+
if (!props.domainRoot)
|
|
282
|
+
throw new Error('domainRoot is required for createGroupedApiActionLambda');
|
|
283
|
+
const vpcConfig = props.vpc
|
|
284
|
+
? {
|
|
285
|
+
vpc: props.vpc,
|
|
286
|
+
securityGroups: props.securityGroups,
|
|
287
|
+
...(props.subnetSelection ? { vpcSubnets: props.subnetSelection } : {}),
|
|
288
|
+
}
|
|
289
|
+
: {};
|
|
290
|
+
const logRetention = toLogRetention(props.logRetentionDays ?? 30);
|
|
291
|
+
const powertoolsLayerArn = `arn:aws:lambda:${cdk.Stack.of(scope).region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:26`;
|
|
292
|
+
const serviceName = `${props.domainId}-action-api`;
|
|
293
|
+
const entryDir = createTempEntryDir();
|
|
294
|
+
const entryPath = join(entryDir, 'entry.ts');
|
|
295
|
+
const entryContent = buildGroupedApiActionEntryContent(props.domainId, props.apiEntries, entryDir, props.domainRoot);
|
|
296
|
+
writeFileSync(entryPath, entryContent, 'utf8');
|
|
297
|
+
const powertoolsLayer = lambda.LayerVersion.fromLayerVersionArn(scope, `PowertoolsLayer${props.domainId}-action-api`, powertoolsLayerArn);
|
|
298
|
+
return new lambdaNode.NodejsFunction(scope, `${props.domainId}-action-api`, {
|
|
299
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
300
|
+
architecture: lambda.Architecture.ARM_64,
|
|
301
|
+
entry: entryPath,
|
|
302
|
+
handler: 'handler',
|
|
303
|
+
layers: [powertoolsLayer],
|
|
304
|
+
bundling: {
|
|
305
|
+
minify: true,
|
|
306
|
+
sourceMap: true,
|
|
307
|
+
externalModules: ['@aws-sdk/*'],
|
|
308
|
+
},
|
|
309
|
+
environment: {
|
|
310
|
+
...props.environment,
|
|
311
|
+
POWERTOOLS_SERVICE_NAME: serviceName,
|
|
312
|
+
POWERTOOLS_LOG_LEVEL: 'INFO',
|
|
313
|
+
TIB_EVENT_BUS_ARN: props.eventBusArn,
|
|
314
|
+
},
|
|
315
|
+
timeout: cdk.Duration.seconds(30),
|
|
316
|
+
memorySize: 256,
|
|
317
|
+
reservedConcurrentExecutions: props.reservedConcurrency,
|
|
318
|
+
logRetention,
|
|
319
|
+
...vpcConfig,
|
|
320
|
+
});
|
|
321
|
+
}
|