@mettlecast/domain-cdk-packer 0.2.92 → 0.2.94
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';
|
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|