@teaminabottle/domain-cdk-packer 0.1.0
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/README.md +83 -0
- package/dist/DomainStack.d.ts +36 -0
- package/dist/DomainStack.js +360 -0
- package/dist/__tests__/domain-stack.test.d.ts +1 -0
- package/dist/__tests__/domain-stack.test.js +138 -0
- package/dist/__tests__/lambda-factory.test.d.ts +1 -0
- package/dist/__tests__/lambda-factory.test.js +30 -0
- package/dist/__tests__/pack-flows.test.d.ts +1 -0
- package/dist/__tests__/pack-flows.test.js +121 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +34 -0
- package/dist/__tests__/step-functions-codegen.test.d.ts +1 -0
- package/dist/__tests__/step-functions-codegen.test.js +159 -0
- package/dist/constructs/action-construct.d.ts +30 -0
- package/dist/constructs/action-construct.js +32 -0
- package/dist/constructs/api-construct.d.ts +30 -0
- package/dist/constructs/api-construct.js +64 -0
- package/dist/constructs/job-construct.d.ts +33 -0
- package/dist/constructs/job-construct.js +54 -0
- package/dist/constructs/schedule-construct.d.ts +27 -0
- package/dist/constructs/schedule-construct.js +54 -0
- package/dist/constructs/subscriber-construct.d.ts +30 -0
- package/dist/constructs/subscriber-construct.js +63 -0
- package/dist/constructs/webhook-construct.d.ts +33 -0
- package/dist/constructs/webhook-construct.js +50 -0
- package/dist/flow-registry.d.ts +78 -0
- package/dist/flow-registry.js +2 -0
- package/dist/grouped-lambda-factory.d.ts +34 -0
- package/dist/grouped-lambda-factory.js +40 -0
- package/dist/iam/iam-policy-builder.d.ts +64 -0
- package/dist/iam/iam-policy-builder.js +132 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +13 -0
- package/dist/lambda-factory.d.ts +46 -0
- package/dist/lambda-factory.js +72 -0
- package/dist/pack-domain.d.ts +30 -0
- package/dist/pack-domain.js +30 -0
- package/dist/pack-flows.d.ts +26 -0
- package/dist/pack-flows.js +112 -0
- package/dist/registry.d.ts +209 -0
- package/dist/registry.js +1 -0
- package/dist/step-functions-codegen.d.ts +65 -0
- package/dist/step-functions-codegen.js +55 -0
- package/package.json +31 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FlowsStack, packFlows } from '../pack-flows.js';
|
|
3
|
+
import * as cdk from 'aws-cdk-lib';
|
|
4
|
+
describe('packFlows', () => {
|
|
5
|
+
it('creates a FlowsStack from a registry', () => {
|
|
6
|
+
const app = new cdk.App();
|
|
7
|
+
const registry = {
|
|
8
|
+
schemaVersion: '1',
|
|
9
|
+
flows: [
|
|
10
|
+
{
|
|
11
|
+
id: 'test-flow',
|
|
12
|
+
name: 'Test Flow',
|
|
13
|
+
steps: [
|
|
14
|
+
{
|
|
15
|
+
type: 'flow-control',
|
|
16
|
+
control: 'succeed',
|
|
17
|
+
name: 'End',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
const stack = packFlows(registry, app, 'test-flows', {
|
|
24
|
+
domainLambdaArns: {},
|
|
25
|
+
eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
|
|
26
|
+
});
|
|
27
|
+
expect(stack).toBeInstanceOf(FlowsStack);
|
|
28
|
+
expect(stack.flowArnMap).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
it('throws when domain-action step references missing Lambda ARN', () => {
|
|
31
|
+
const app = new cdk.App();
|
|
32
|
+
const registry = {
|
|
33
|
+
schemaVersion: '1',
|
|
34
|
+
flows: [
|
|
35
|
+
{
|
|
36
|
+
id: 'action-flow',
|
|
37
|
+
name: 'Action Flow',
|
|
38
|
+
steps: [
|
|
39
|
+
{
|
|
40
|
+
type: 'domain-action',
|
|
41
|
+
name: 'RunAction',
|
|
42
|
+
domainId: 'example',
|
|
43
|
+
actionId: 'process',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
expect(() => {
|
|
50
|
+
packFlows(registry, app, 'test-flows', {
|
|
51
|
+
domainLambdaArns: {},
|
|
52
|
+
eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
|
|
53
|
+
});
|
|
54
|
+
}).toThrow(/no Lambda ARN for example-action/);
|
|
55
|
+
});
|
|
56
|
+
it('translates domain-action step with Lambda ARN', () => {
|
|
57
|
+
const app = new cdk.App();
|
|
58
|
+
const registry = {
|
|
59
|
+
schemaVersion: '1',
|
|
60
|
+
flows: [
|
|
61
|
+
{
|
|
62
|
+
id: 'action-flow',
|
|
63
|
+
name: 'Action Flow',
|
|
64
|
+
steps: [
|
|
65
|
+
{
|
|
66
|
+
type: 'domain-action',
|
|
67
|
+
name: 'RunAction',
|
|
68
|
+
domainId: 'example',
|
|
69
|
+
actionId: 'process',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const stack = packFlows(registry, app, 'test-flows', {
|
|
76
|
+
domainLambdaArns: {
|
|
77
|
+
'example-action': 'arn:aws:lambda:us-east-1:123456789012:function:test',
|
|
78
|
+
},
|
|
79
|
+
eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
|
|
80
|
+
});
|
|
81
|
+
expect(stack.flowArnMap['action-flow']).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
it('translates domain-event step to putEvents', () => {
|
|
84
|
+
const app = new cdk.App();
|
|
85
|
+
const registry = {
|
|
86
|
+
schemaVersion: '1',
|
|
87
|
+
flows: [
|
|
88
|
+
{
|
|
89
|
+
id: 'event-flow',
|
|
90
|
+
name: 'Event Flow',
|
|
91
|
+
steps: [
|
|
92
|
+
{
|
|
93
|
+
type: 'domain-event',
|
|
94
|
+
name: 'EmitEvent',
|
|
95
|
+
eventId: 'example.completed',
|
|
96
|
+
payload: { status: 'ok' },
|
|
97
|
+
version: 1,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const stack = packFlows(registry, app, 'test-flows', {
|
|
104
|
+
domainLambdaArns: {},
|
|
105
|
+
eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
|
|
106
|
+
});
|
|
107
|
+
expect(stack.flowArnMap['event-flow']).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
it('handles empty flow registry gracefully', () => {
|
|
110
|
+
const app = new cdk.App();
|
|
111
|
+
const registry = {
|
|
112
|
+
schemaVersion: '1',
|
|
113
|
+
flows: [],
|
|
114
|
+
};
|
|
115
|
+
const stack = packFlows(registry, app, 'test-flows', {
|
|
116
|
+
domainLambdaArns: {},
|
|
117
|
+
eventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test',
|
|
118
|
+
});
|
|
119
|
+
expect(Object.keys(stack.flowArnMap)).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
describe('DomainRegistry type', () => {
|
|
3
|
+
it('accepts a valid minimal registry object', () => {
|
|
4
|
+
const r = {
|
|
5
|
+
schemaVersion: '1',
|
|
6
|
+
domainRoot: '/workspace/my-domain',
|
|
7
|
+
domain: { id: 'my-domain', kind: 'domain', name: 'My Domain', tenancy: 'required' },
|
|
8
|
+
apis: [],
|
|
9
|
+
webhooks: [],
|
|
10
|
+
subscribers: [],
|
|
11
|
+
schedules: [],
|
|
12
|
+
jobs: [],
|
|
13
|
+
actions: [],
|
|
14
|
+
integrations: [],
|
|
15
|
+
events: [],
|
|
16
|
+
};
|
|
17
|
+
expect(r.schemaVersion).toBe('1');
|
|
18
|
+
expect(r.domain.kind).toBe('domain');
|
|
19
|
+
});
|
|
20
|
+
it('discriminates RegistryEntry union by kind', () => {
|
|
21
|
+
const entries = [
|
|
22
|
+
{ id: 'test-api', kind: 'api', handlerFile: 'src/api.ts', path: '/test', method: 'GET', authType: 'jwt' },
|
|
23
|
+
{ id: 'test-job', kind: 'job', handlerFile: 'src/job.ts', maxRetries: 3, visibilityTimeoutSeconds: 30 },
|
|
24
|
+
];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.kind === 'api') {
|
|
27
|
+
expect(entry.path).toBeDefined();
|
|
28
|
+
}
|
|
29
|
+
if (entry.kind === 'job') {
|
|
30
|
+
expect(entry.maxRetries).toBeDefined();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { StepFunctionsCodegen } from '../step-functions-codegen.js';
|
|
3
|
+
describe('StepFunctionsCodegen', () => {
|
|
4
|
+
const codegen = new StepFunctionsCodegen();
|
|
5
|
+
describe('generateTaskState', () => {
|
|
6
|
+
it('should set Next when node has next property', () => {
|
|
7
|
+
const node = {
|
|
8
|
+
type: 'domain-action',
|
|
9
|
+
domainId: 'payments',
|
|
10
|
+
actionId: 'charge-card',
|
|
11
|
+
next: 'Step2',
|
|
12
|
+
};
|
|
13
|
+
const state = codegen.generateTaskState(node);
|
|
14
|
+
expect(state.Next).toBe('Step2');
|
|
15
|
+
expect(state.End).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
it('should set End=true when node has no next property', () => {
|
|
18
|
+
const node = {
|
|
19
|
+
type: 'domain-action',
|
|
20
|
+
domainId: 'payments',
|
|
21
|
+
actionId: 'charge-card',
|
|
22
|
+
};
|
|
23
|
+
const state = codegen.generateTaskState(node);
|
|
24
|
+
expect(state.End).toBe(true);
|
|
25
|
+
expect(state.Next).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
it('should generate correct FunctionName from domainId and actionId', () => {
|
|
28
|
+
const node = {
|
|
29
|
+
type: 'domain-action',
|
|
30
|
+
domainId: 'payments',
|
|
31
|
+
actionId: 'charge-card',
|
|
32
|
+
};
|
|
33
|
+
const state = codegen.generateTaskState(node);
|
|
34
|
+
expect(state.Parameters?.FunctionName).toBe('payments-charge-card');
|
|
35
|
+
});
|
|
36
|
+
it('should use label as Comment when provided', () => {
|
|
37
|
+
const node = {
|
|
38
|
+
type: 'domain-action',
|
|
39
|
+
domainId: 'payments',
|
|
40
|
+
actionId: 'charge-card',
|
|
41
|
+
label: 'Charge the card',
|
|
42
|
+
};
|
|
43
|
+
const state = codegen.generateTaskState(node);
|
|
44
|
+
expect(state.Comment).toBe('Charge the card');
|
|
45
|
+
});
|
|
46
|
+
it('should use default comment format when label is not provided', () => {
|
|
47
|
+
const node = {
|
|
48
|
+
type: 'domain-action',
|
|
49
|
+
domainId: 'payments',
|
|
50
|
+
actionId: 'charge-card',
|
|
51
|
+
};
|
|
52
|
+
const state = codegen.generateTaskState(node);
|
|
53
|
+
expect(state.Comment).toBe('Invoke domain action payments/charge-card');
|
|
54
|
+
});
|
|
55
|
+
it('should include standard Task fields in the state', () => {
|
|
56
|
+
const node = {
|
|
57
|
+
type: 'domain-action',
|
|
58
|
+
domainId: 'payments',
|
|
59
|
+
actionId: 'charge-card',
|
|
60
|
+
};
|
|
61
|
+
const state = codegen.generateTaskState(node);
|
|
62
|
+
expect(state.Type).toBe('Task');
|
|
63
|
+
expect(state.Resource).toBe('arn:aws:states:::lambda:invoke');
|
|
64
|
+
expect(state.Parameters?.['Payload.$']).toBe('$');
|
|
65
|
+
});
|
|
66
|
+
it('should include node parameters in the Parameters object', () => {
|
|
67
|
+
const node = {
|
|
68
|
+
type: 'domain-action',
|
|
69
|
+
domainId: 'payments',
|
|
70
|
+
actionId: 'charge-card',
|
|
71
|
+
parameters: { amount: 100, currency: 'USD' },
|
|
72
|
+
};
|
|
73
|
+
const state = codegen.generateTaskState(node);
|
|
74
|
+
expect(state.Parameters?.amount).toBe(100);
|
|
75
|
+
expect(state.Parameters?.currency).toBe('USD');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('generateStateMachine', () => {
|
|
79
|
+
it('should set StartAt to first node label by default', () => {
|
|
80
|
+
const nodes = [
|
|
81
|
+
{
|
|
82
|
+
type: 'domain-action',
|
|
83
|
+
domainId: 'payments',
|
|
84
|
+
actionId: 'charge-card',
|
|
85
|
+
label: 'Charge Card',
|
|
86
|
+
next: 'Validate Payment',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'domain-action',
|
|
90
|
+
domainId: 'payments',
|
|
91
|
+
actionId: 'validate-payment',
|
|
92
|
+
label: 'Validate Payment',
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
const result = codegen.generateStateMachine(nodes);
|
|
96
|
+
expect(result.StartAt).toBe('Charge Card');
|
|
97
|
+
});
|
|
98
|
+
it('should set StartAt to first node actionId when label is missing', () => {
|
|
99
|
+
const nodes = [
|
|
100
|
+
{
|
|
101
|
+
type: 'domain-action',
|
|
102
|
+
domainId: 'payments',
|
|
103
|
+
actionId: 'charge-card',
|
|
104
|
+
next: 'payments/validate-payment',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'domain-action',
|
|
108
|
+
domainId: 'payments',
|
|
109
|
+
actionId: 'validate-payment',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const result = codegen.generateStateMachine(nodes);
|
|
113
|
+
expect(result.StartAt).toBe('payments/charge-card');
|
|
114
|
+
});
|
|
115
|
+
it('should include all nodes as states with correct state names', () => {
|
|
116
|
+
const nodes = [
|
|
117
|
+
{
|
|
118
|
+
type: 'domain-action',
|
|
119
|
+
domainId: 'payments',
|
|
120
|
+
actionId: 'charge-card',
|
|
121
|
+
label: 'Charge Card',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'domain-action',
|
|
125
|
+
domainId: 'payments',
|
|
126
|
+
actionId: 'validate-payment',
|
|
127
|
+
label: 'Validate Payment',
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const result = codegen.generateStateMachine(nodes);
|
|
131
|
+
expect(Object.keys(result.States)).toHaveLength(2);
|
|
132
|
+
expect(result.States['Charge Card']).toBeDefined();
|
|
133
|
+
expect(result.States['Validate Payment']).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
it('should use custom startAt parameter when provided', () => {
|
|
136
|
+
const nodes = [
|
|
137
|
+
{
|
|
138
|
+
type: 'domain-action',
|
|
139
|
+
domainId: 'payments',
|
|
140
|
+
actionId: 'charge-card',
|
|
141
|
+
label: 'Charge Card',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: 'domain-action',
|
|
145
|
+
domainId: 'payments',
|
|
146
|
+
actionId: 'validate-payment',
|
|
147
|
+
label: 'Validate Payment',
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const result = codegen.generateStateMachine(nodes, 'CustomStart');
|
|
151
|
+
expect(result.StartAt).toBe('CustomStart');
|
|
152
|
+
});
|
|
153
|
+
it('should handle empty nodes array', () => {
|
|
154
|
+
const result = codegen.generateStateMachine([]);
|
|
155
|
+
expect(result.StartAt).toBe('Start');
|
|
156
|
+
expect(Object.keys(result.States)).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for ActionConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface ActionConstructProps {
|
|
10
|
+
/** Domain registry containing all action entries. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
13
|
+
lambdaFactory: LambdaFactory;
|
|
14
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
15
|
+
iamBuilder: IamPolicyBuilder;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* CDK Construct that synthesises one Lambda per registered action, with optional Function URL for workspace-visible actions.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ActionConstruct extends Construct {
|
|
21
|
+
/** Lambda function per action id. */
|
|
22
|
+
readonly functions: Map<string, lambdaNode.NodejsFunction>;
|
|
23
|
+
/**
|
|
24
|
+
* Create a new ActionConstruct instance.
|
|
25
|
+
* @param scope - Parent CDK scope.
|
|
26
|
+
* @param id - Construct identifier.
|
|
27
|
+
* @param props - Construct properties.
|
|
28
|
+
*/
|
|
29
|
+
constructor(scope: Construct, id: string, props: ActionConstructProps);
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
/**
|
|
4
|
+
* CDK Construct that synthesises one Lambda per registered action, with optional Function URL for workspace-visible actions.
|
|
5
|
+
*/
|
|
6
|
+
export class ActionConstruct extends Construct {
|
|
7
|
+
/** Lambda function per action id. */
|
|
8
|
+
functions;
|
|
9
|
+
/**
|
|
10
|
+
* Create a new ActionConstruct instance.
|
|
11
|
+
* @param scope - Parent CDK scope.
|
|
12
|
+
* @param id - Construct identifier.
|
|
13
|
+
* @param props - Construct properties.
|
|
14
|
+
*/
|
|
15
|
+
constructor(scope, id, props) {
|
|
16
|
+
super(scope, id);
|
|
17
|
+
this.functions = new Map();
|
|
18
|
+
for (const entry of props.registry.actions) {
|
|
19
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
20
|
+
this.functions.set(entry.id, fn);
|
|
21
|
+
props.iamBuilder.forAction().forEach((statement) => {
|
|
22
|
+
fn.addToRolePolicy(statement);
|
|
23
|
+
});
|
|
24
|
+
if (entry.visibility === 'workspace') {
|
|
25
|
+
fn.addFunctionUrl({
|
|
26
|
+
authType: lambda.FunctionUrlAuthType.NONE,
|
|
27
|
+
cors: { allowedOrigins: ['*'], allowedMethods: [lambda.HttpMethod.ALL] },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for ApiConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApiConstructProps {
|
|
10
|
+
/** Domain registry containing all API entries. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Shared HttpApi to route requests to Lambdas. */
|
|
13
|
+
httpApi: apigwv2.HttpApi;
|
|
14
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
15
|
+
lambdaFactory: LambdaFactory;
|
|
16
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
17
|
+
iamBuilder: IamPolicyBuilder;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* CDK Construct that synthesises one Lambda per registered API and wires each to the shared HttpApi.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ApiConstruct extends Construct {
|
|
23
|
+
/**
|
|
24
|
+
* Create a new ApiConstruct instance.
|
|
25
|
+
* @param scope - Parent CDK scope.
|
|
26
|
+
* @param id - Construct identifier.
|
|
27
|
+
* @param props - Construct properties.
|
|
28
|
+
*/
|
|
29
|
+
constructor(scope: Construct, id: string, props: ApiConstructProps);
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
2
|
+
import * as apigwv2integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
/**
|
|
5
|
+
* CDK Construct that synthesises one Lambda per registered API and wires each to the shared HttpApi.
|
|
6
|
+
*/
|
|
7
|
+
export class ApiConstruct extends Construct {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new ApiConstruct instance.
|
|
10
|
+
* @param scope - Parent CDK scope.
|
|
11
|
+
* @param id - Construct identifier.
|
|
12
|
+
* @param props - Construct properties.
|
|
13
|
+
*/
|
|
14
|
+
constructor(scope, id, props) {
|
|
15
|
+
super(scope, id);
|
|
16
|
+
for (const entry of props.registry.apis) {
|
|
17
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
18
|
+
props.iamBuilder.forApi().forEach((statement) => {
|
|
19
|
+
fn.addToRolePolicy(statement);
|
|
20
|
+
});
|
|
21
|
+
props.httpApi.addRoutes({
|
|
22
|
+
path: entry.path,
|
|
23
|
+
methods: [toHttpMethod(entry.method)],
|
|
24
|
+
integration: new apigwv2integrations.HttpLambdaIntegration(`${id}${toPascalCase(entry.id)}Integration`, fn),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convert kebab-case or snake_case string to PascalCase.
|
|
31
|
+
* @param s - Input string.
|
|
32
|
+
* @returns PascalCase string.
|
|
33
|
+
*/
|
|
34
|
+
function toPascalCase(s) {
|
|
35
|
+
return s
|
|
36
|
+
.split(/[-_]/)
|
|
37
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
38
|
+
.join('');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert HTTP method string to ApiGatewayV2 HttpMethod enum.
|
|
42
|
+
* @param method - HTTP method string (e.g., 'GET', 'POST').
|
|
43
|
+
* @returns HttpMethod enum value.
|
|
44
|
+
*/
|
|
45
|
+
function toHttpMethod(method) {
|
|
46
|
+
switch (method.toUpperCase()) {
|
|
47
|
+
case 'GET':
|
|
48
|
+
return apigwv2.HttpMethod.GET;
|
|
49
|
+
case 'POST':
|
|
50
|
+
return apigwv2.HttpMethod.POST;
|
|
51
|
+
case 'PUT':
|
|
52
|
+
return apigwv2.HttpMethod.PUT;
|
|
53
|
+
case 'PATCH':
|
|
54
|
+
return apigwv2.HttpMethod.PATCH;
|
|
55
|
+
case 'DELETE':
|
|
56
|
+
return apigwv2.HttpMethod.DELETE;
|
|
57
|
+
case 'HEAD':
|
|
58
|
+
return apigwv2.HttpMethod.HEAD;
|
|
59
|
+
case 'OPTIONS':
|
|
60
|
+
return apigwv2.HttpMethod.OPTIONS;
|
|
61
|
+
default:
|
|
62
|
+
return apigwv2.HttpMethod.ANY;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for JobConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface JobConstructProps {
|
|
10
|
+
/** Domain registry containing job definitions. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Factory for creating Lambda functions. */
|
|
13
|
+
lambdaFactory: LambdaFactory;
|
|
14
|
+
/** Builder for generating IAM policies. */
|
|
15
|
+
iamBuilder: IamPolicyBuilder;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* CDK Construct that synthesizes Lambda functions with SQS queues, dead-letter queues,
|
|
19
|
+
* and event source mappings for background jobs defined in the domain registry.
|
|
20
|
+
*/
|
|
21
|
+
export declare class JobConstruct extends Construct {
|
|
22
|
+
/**
|
|
23
|
+
* Map of job IDs to their corresponding SQS queues.
|
|
24
|
+
*/
|
|
25
|
+
readonly queues: Map<string, sqs.Queue>;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new JobConstruct.
|
|
28
|
+
* @param scope - Parent CDK scope
|
|
29
|
+
* @param id - Construct identifier
|
|
30
|
+
* @param props - Configuration properties
|
|
31
|
+
*/
|
|
32
|
+
constructor(scope: Construct, id: string, props: JobConstructProps);
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
3
|
+
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
4
|
+
import { Construct } from 'constructs';
|
|
5
|
+
/**
|
|
6
|
+
* CDK Construct that synthesizes Lambda functions with SQS queues, dead-letter queues,
|
|
7
|
+
* and event source mappings for background jobs defined in the domain registry.
|
|
8
|
+
*/
|
|
9
|
+
export class JobConstruct extends Construct {
|
|
10
|
+
/**
|
|
11
|
+
* Map of job IDs to their corresponding SQS queues.
|
|
12
|
+
*/
|
|
13
|
+
queues;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new JobConstruct.
|
|
16
|
+
* @param scope - Parent CDK scope
|
|
17
|
+
* @param id - Construct identifier
|
|
18
|
+
* @param props - Configuration properties
|
|
19
|
+
*/
|
|
20
|
+
constructor(scope, id, props) {
|
|
21
|
+
super(scope, id);
|
|
22
|
+
this.queues = new Map();
|
|
23
|
+
for (const entry of props.registry.jobs) {
|
|
24
|
+
const pascalId = toPascalCase(entry.id);
|
|
25
|
+
const dlq = new sqs.Queue(this, `${pascalId}Dlq`, {
|
|
26
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
27
|
+
});
|
|
28
|
+
const queue = new sqs.Queue(this, `${pascalId}Queue`, {
|
|
29
|
+
visibilityTimeout: cdk.Duration.seconds(entry.visibilityTimeoutSeconds),
|
|
30
|
+
deadLetterQueue: {
|
|
31
|
+
queue: dlq,
|
|
32
|
+
maxReceiveCount: entry.maxRetries,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
this.queues.set(entry.id, queue);
|
|
36
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
37
|
+
props.iamBuilder
|
|
38
|
+
.forJob({ queueArn: queue.queueArn, dlqArn: dlq.queueArn })
|
|
39
|
+
.forEach((s) => fn.addToRolePolicy(s));
|
|
40
|
+
fn.addEventSource(new lambdaEventSources.SqsEventSource(queue, { batchSize: 1 }));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a kebab-case or snake_case string to PascalCase.
|
|
46
|
+
* @param s - Input string to convert
|
|
47
|
+
* @returns PascalCase string
|
|
48
|
+
*/
|
|
49
|
+
function toPascalCase(s) {
|
|
50
|
+
return s
|
|
51
|
+
.split(/[-_]/)
|
|
52
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
53
|
+
.join('');
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
3
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
4
|
+
import type { DomainRegistry } from '../registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration properties for ScheduleConstruct.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScheduleConstructProps {
|
|
9
|
+
/** Domain registry containing schedule definitions */
|
|
10
|
+
registry: DomainRegistry;
|
|
11
|
+
/** Lambda factory for creating schedule handler functions */
|
|
12
|
+
lambdaFactory: LambdaFactory;
|
|
13
|
+
/** IAM policy builder for schedule handler permissions */
|
|
14
|
+
iamBuilder: IamPolicyBuilder;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* CDK Construct that synthesises Lambda functions and EventBridge Scheduler rules per schedule entry.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ScheduleConstruct extends Construct {
|
|
20
|
+
/**
|
|
21
|
+
* Create a new ScheduleConstruct.
|
|
22
|
+
* @param scope - Parent CDK Construct scope
|
|
23
|
+
* @param id - Logical ID for this construct
|
|
24
|
+
* @param props - Configuration properties
|
|
25
|
+
*/
|
|
26
|
+
constructor(scope: Construct, id: string, props: ScheduleConstructProps);
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
2
|
+
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
/**
|
|
5
|
+
* CDK Construct that synthesises Lambda functions and EventBridge Scheduler rules per schedule entry.
|
|
6
|
+
*/
|
|
7
|
+
export class ScheduleConstruct extends Construct {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new ScheduleConstruct.
|
|
10
|
+
* @param scope - Parent CDK Construct scope
|
|
11
|
+
* @param id - Logical ID for this construct
|
|
12
|
+
* @param props - Configuration properties
|
|
13
|
+
*/
|
|
14
|
+
constructor(scope, id, props) {
|
|
15
|
+
super(scope, id);
|
|
16
|
+
for (const entry of props.registry.schedules) {
|
|
17
|
+
const pascalId = toPascalCase(entry.id);
|
|
18
|
+
// Create the Lambda function for this schedule
|
|
19
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
20
|
+
// Attach schedule-specific IAM policies
|
|
21
|
+
props.iamBuilder.forSchedule().forEach((statement) => {
|
|
22
|
+
fn.addToRolePolicy(statement);
|
|
23
|
+
});
|
|
24
|
+
// Create a dedicated IAM role for the EventBridge Scheduler to assume
|
|
25
|
+
const schedulerRole = new iam.Role(this, `${pascalId}SchedulerRole`, {
|
|
26
|
+
assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com'),
|
|
27
|
+
});
|
|
28
|
+
// Grant the scheduler role permission to invoke the Lambda
|
|
29
|
+
fn.grantInvoke(schedulerRole);
|
|
30
|
+
// Create the EventBridge Scheduler rule
|
|
31
|
+
new scheduler.CfnSchedule(this, `${pascalId}Schedule`, {
|
|
32
|
+
scheduleExpression: entry.cron,
|
|
33
|
+
flexibleTimeWindow: { mode: 'OFF' },
|
|
34
|
+
state: entry.enabled ? 'ENABLED' : 'DISABLED',
|
|
35
|
+
target: {
|
|
36
|
+
arn: fn.functionArn,
|
|
37
|
+
roleArn: schedulerRole.roleArn,
|
|
38
|
+
input: JSON.stringify({}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a kebab-case or snake_case string to PascalCase.
|
|
46
|
+
* @param s - Input string
|
|
47
|
+
* @returns PascalCase string
|
|
48
|
+
*/
|
|
49
|
+
function toPascalCase(s) {
|
|
50
|
+
return s
|
|
51
|
+
.split(/[-_]/)
|
|
52
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
53
|
+
.join('');
|
|
54
|
+
}
|