@intentius/chant-lexicon-aws 0.45.0 → 0.49.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/dist/api/read-client.d.ts +10 -2
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +26 -1
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/bucket-deployment.d.ts +82 -0
- package/dist/composites/bucket-deployment.d.ts.map +1 -0
- package/dist/composites/dynamodb-table.d.ts +45 -0
- package/dist/composites/dynamodb-table.d.ts.map +1 -0
- package/dist/composites/ec2-instance-bundle.d.ts +61 -0
- package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
- package/dist/composites/ecr-repository.d.ts +49 -0
- package/dist/composites/ecr-repository.d.ts.map +1 -0
- package/dist/composites/fargate-service.d.ts +16 -0
- package/dist/composites/fargate-service.d.ts.map +1 -1
- package/dist/composites/index.d.ts +16 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/lambda-function.d.ts +4 -4
- package/dist/composites/lambda-function.d.ts.map +1 -1
- package/dist/composites/monitoring-stack.d.ts +99 -0
- package/dist/composites/monitoring-stack.d.ts.map +1 -0
- package/dist/composites/nlb-service.d.ts +48 -0
- package/dist/composites/nlb-service.d.ts.map +1 -0
- package/dist/composites/step-functions-workflow.d.ts +36 -0
- package/dist/composites/step-functions-workflow.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +45 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-topology.d.ts +104 -0
- package/dist/deep-topology.d.ts.map +1 -0
- package/dist/effect-receipt-row.d.ts +92 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +9 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +24 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw059.d.ts +36 -0
- package/dist/lint/post-synth/waw059.d.ts.map +1 -0
- package/dist/lint/post-synth/waw060.d.ts +16 -0
- package/dist/lint/post-synth/waw060.d.ts.map +1 -0
- package/dist/lint/post-synth/waw061.d.ts +19 -0
- package/dist/lint/post-synth/waw061.d.ts.map +1 -0
- package/dist/lint/post-synth/waw062.d.ts +19 -0
- package/dist/lint/post-synth/waw062.d.ts.map +1 -0
- package/dist/lint/post-synth/waw063.d.ts +35 -0
- package/dist/lint/post-synth/waw063.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +5 -0
- package/dist/okf/rules/WAW059.md +25 -0
- package/dist/okf/rules/WAW060.md +17 -0
- package/dist/okf/rules/WAW061.md +17 -0
- package/dist/okf/rules/WAW062.md +27 -0
- package/dist/okf/rules/WAW063.md +19 -0
- package/dist/okf/types/Action.md +2 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/GlobalTable.md +4 -0
- package/dist/okf/types/IamPolicy.md +3 -0
- package/dist/okf/types/InstanceProfile.md +4 -0
- package/dist/okf/types/LaunchTemplate.md +1 -0
- package/dist/okf/types/LoadBalancer.md +1 -0
- package/dist/okf/types/ManagedPolicy.md +4 -0
- package/dist/okf/types/Map.md +2 -0
- package/dist/okf/types/Queue.md +1 -0
- package/dist/okf/types/Role.md +3 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +4 -0
- package/dist/okf/types/Table.md +2 -0
- package/dist/okf/types/Type.md +5 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/aws-apply.d.ts +48 -5
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +11 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +18 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +109 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/cf-refs.ts +62 -0
- package/dist/rules/waw059.ts +353 -0
- package/dist/rules/waw060.ts +91 -0
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/teardown.d.ts +85 -0
- package/dist/teardown.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/components/cloud-executor.ts +10 -1
- package/src/composites/agentcore-agent.ts +35 -13
- package/src/composites/bucket-deployment.test.ts +84 -0
- package/src/composites/bucket-deployment.ts +153 -0
- package/src/composites/composites.test.ts +325 -0
- package/src/composites/dynamodb-table.test.ts +154 -0
- package/src/composites/dynamodb-table.ts +124 -0
- package/src/composites/ec2-instance-bundle.ts +134 -0
- package/src/composites/ecr-repository.test.ts +118 -0
- package/src/composites/ecr-repository.ts +117 -0
- package/src/composites/fargate-service.ts +19 -3
- package/src/composites/index.ts +21 -2
- package/src/composites/monitoring-stack.test.ts +130 -0
- package/src/composites/monitoring-stack.ts +164 -0
- package/src/composites/nlb-service.ts +117 -0
- package/src/composites/step-functions-workflow.ts +127 -0
- package/src/deep-observe.test.ts +201 -1
- package/src/deep-observe.ts +204 -0
- package/src/deep-topology.test.ts +392 -0
- package/src/deep-topology.ts +237 -0
- package/src/effect-receipt-row.test.ts +164 -0
- package/src/effect-receipt-row.ts +147 -0
- package/src/index.ts +28 -7
- package/src/lifecycle-integration.test.ts +4 -0
- package/src/lint/audit-catalog.ts +11 -0
- package/src/lint/post-synth/cf-refs.ts +62 -0
- package/src/lint/post-synth/index.ts +10 -0
- package/src/lint/post-synth/waw059.test.ts +309 -0
- package/src/lint/post-synth/waw059.ts +353 -0
- package/src/lint/post-synth/waw060.test.ts +131 -0
- package/src/lint/post-synth/waw060.ts +91 -0
- package/src/lint/post-synth/waw061.test.ts +99 -0
- package/src/lint/post-synth/waw061.ts +67 -0
- package/src/lint/post-synth/waw062.test.ts +109 -0
- package/src/lint/post-synth/waw062.ts +0 -0
- package/src/lint/post-synth/waw063.test.ts +220 -0
- package/src/lint/post-synth/waw063.ts +0 -0
- package/src/op/activities/aws-apply.test.ts +111 -0
- package/src/op/activities/aws-apply.ts +100 -6
- package/src/op/activities/index.ts +20 -3
- package/src/ownership.test.ts +24 -1
- package/src/ownership.ts +37 -0
- package/src/plugin.ts +59 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer-ownership.test.ts +18 -0
- package/src/serializer.ts +114 -3
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/teardown.test.ts +258 -0
- package/src/teardown.ts +276 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BucketDeployment — the seeded-bucket shape: an S3 bucket sized to receive
|
|
3
|
+
* deploy-time content, encrypted and access-blocked by default, its
|
|
4
|
+
* deletion behaviour and (optionally) its public-read policy declared up
|
|
5
|
+
* front rather than left to CloudFormation's own defaults.
|
|
6
|
+
*
|
|
7
|
+
* `s3deploy.BucketDeployment` is the most-used CDK pattern across the
|
|
8
|
+
* aws-bench corpus (37 instantiations across 8 apps, chant#1139) and it
|
|
9
|
+
* names two things CDK bundles together that chant keeps apart on purpose:
|
|
10
|
+
*
|
|
11
|
+
* - The bucket itself: `AWS::S3::Bucket` plus its encryption, access-block
|
|
12
|
+
* and (for the static-site shape) website/public-policy configuration.
|
|
13
|
+
* That is a fixed set of CloudFormation resources, so it is what this
|
|
14
|
+
* composite declares — the same shape `LambdaS3` (../composites) already
|
|
15
|
+
* gives a bucket, minus the Lambda trigger wiring.
|
|
16
|
+
* - Uploading local files into it at deploy time. CDK does this with a
|
|
17
|
+
* Lambda-backed custom resource (`cr.AwsCustomResource`, the same family
|
|
18
|
+
* the epic notes maps to chant Ops rather than composites — chant has no
|
|
19
|
+
* CloudFormation custom-resource machinery, and inventing one just to
|
|
20
|
+
* reproduce a `PutObject` loop would be new plumbing for a solved
|
|
21
|
+
* problem). chant already ships this as a deploy-time **capability**,
|
|
22
|
+
* `s3-sync` (`../components/apply.ts`'s `createS3SyncCapability`, backed
|
|
23
|
+
* by the real `aws s3 sync`/`aws s3 cp` CLI through `CloudExecutor`, with
|
|
24
|
+
* the typed step-builder `s3Sync` in `../components/builders.ts`) — it
|
|
25
|
+
* was defined for exactly this and, before this composite, had no
|
|
26
|
+
* composite pairing it with a bucket to sync into. A component composes
|
|
27
|
+
* the two:
|
|
28
|
+
*
|
|
29
|
+
* const seeded = BucketDeployment({ bucketName: Sub`${AWS.StackName}-site` });
|
|
30
|
+
* // ...component `deploy` phase:
|
|
31
|
+
* s3Sync({ from: "archive:site-build", to: Sub`s3://${seeded.bucket.Ref}` })
|
|
32
|
+
*
|
|
33
|
+
* That split — graph-time declaration here, deploy-time action in the
|
|
34
|
+
* component layer — is the existing precedent (`s3-sync` shipped in #566
|
|
35
|
+
* unused by any composite until this one), not a new one invented for this
|
|
36
|
+
* composite.
|
|
37
|
+
*
|
|
38
|
+
* "Cleanup" (the epic's third word) is `removalPolicy`, CloudFormation's own
|
|
39
|
+
* `DeletionPolicy` on the bucket resource. CDK's `autoDeleteObjects` — the
|
|
40
|
+
* Lambda-backed hook that empties a bucket before CloudFormation is allowed
|
|
41
|
+
* to delete it — has the same custom-resource shape as content upload and
|
|
42
|
+
* the same answer: out of scope here, and worth an `s3 rm --recursive`
|
|
43
|
+
* step ahead of a stack teardown in the component that owns the deploy.
|
|
44
|
+
*/
|
|
45
|
+
import { Composite, mergeDefaults, type Value } from "@intentius/chant";
|
|
46
|
+
import {
|
|
47
|
+
Bucket,
|
|
48
|
+
Bucket_BucketEncryption,
|
|
49
|
+
Bucket_ServerSideEncryptionRule,
|
|
50
|
+
Bucket_ServerSideEncryptionByDefault,
|
|
51
|
+
Bucket_PublicAccessBlockConfiguration,
|
|
52
|
+
Bucket_VersioningConfiguration,
|
|
53
|
+
Bucket_WebsiteConfiguration,
|
|
54
|
+
S3BucketPolicy,
|
|
55
|
+
} from "../generated";
|
|
56
|
+
import { Sub } from "../intrinsics";
|
|
57
|
+
|
|
58
|
+
export interface BucketDeploymentProps {
|
|
59
|
+
/** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). Omitted lets CloudFormation generate one. */
|
|
60
|
+
bucketName?: Value<string>;
|
|
61
|
+
/** Enable S3 object versioning. Default: false. */
|
|
62
|
+
versioned?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Static-website hosting: sets `WebsiteConfiguration` and — since a
|
|
65
|
+
* website bucket is read by anonymous visitors, not IAM principals —
|
|
66
|
+
* opens the public-access block and attaches a public-read
|
|
67
|
+
* `S3BucketPolicy` scoped to `s3:GetObject`. Omitted, the bucket stays
|
|
68
|
+
* fully private, matching every other composite bucket's default
|
|
69
|
+
* (`LambdaS3`, ../composites).
|
|
70
|
+
*/
|
|
71
|
+
website?: {
|
|
72
|
+
indexDocument: string;
|
|
73
|
+
errorDocument?: string;
|
|
74
|
+
};
|
|
75
|
+
/** What CloudFormation does to the bucket when the stack (or the resource) is removed. Default: "retain" — CloudFormation's own default, and the safe one for a bucket a deploy has just put content into. */
|
|
76
|
+
removalPolicy?: "retain" | "destroy";
|
|
77
|
+
tags?: Array<{ Key: string; Value: string }>;
|
|
78
|
+
defaults?: {
|
|
79
|
+
bucket?: Partial<ConstructorParameters<typeof Bucket>[0]>;
|
|
80
|
+
bucketPolicy?: Partial<ConstructorParameters<typeof S3BucketPolicy>[0]>;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type BucketDeploymentResult =
|
|
85
|
+
| { bucket: InstanceType<typeof Bucket> }
|
|
86
|
+
| { bucket: InstanceType<typeof Bucket>; bucketPolicy: InstanceType<typeof S3BucketPolicy> };
|
|
87
|
+
|
|
88
|
+
export const BucketDeployment = Composite<BucketDeploymentProps>((props) => {
|
|
89
|
+
const { defaults } = props;
|
|
90
|
+
const publicRead = props.website !== undefined;
|
|
91
|
+
|
|
92
|
+
const encryptionDefault = new Bucket_ServerSideEncryptionByDefault({ SSEAlgorithm: "AES256" });
|
|
93
|
+
const encryptionRule = new Bucket_ServerSideEncryptionRule({ ServerSideEncryptionByDefault: encryptionDefault });
|
|
94
|
+
const bucketEncryption = new Bucket_BucketEncryption({ ServerSideEncryptionConfiguration: [encryptionRule] });
|
|
95
|
+
|
|
96
|
+
// A ternary, not an `if`, so the block-scoped `new` stays an expression
|
|
97
|
+
// (EVL002) — the same reasoning eks-cluster.ts's addon slots document.
|
|
98
|
+
const publicAccessBlock = new Bucket_PublicAccessBlockConfiguration(
|
|
99
|
+
publicRead
|
|
100
|
+
? { BlockPublicAcls: false, BlockPublicPolicy: false, IgnorePublicAcls: false, RestrictPublicBuckets: false }
|
|
101
|
+
: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const versioningConfiguration = props.versioned
|
|
105
|
+
? new Bucket_VersioningConfiguration({ Status: "Enabled" })
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const websiteConfiguration = props.website
|
|
109
|
+
? new Bucket_WebsiteConfiguration({
|
|
110
|
+
IndexDocument: props.website.indexDocument,
|
|
111
|
+
...(props.website.errorDocument ? { ErrorDocument: props.website.errorDocument } : {}),
|
|
112
|
+
})
|
|
113
|
+
: undefined;
|
|
114
|
+
|
|
115
|
+
const bucket = new Bucket(
|
|
116
|
+
mergeDefaults(
|
|
117
|
+
{
|
|
118
|
+
BucketName: props.bucketName,
|
|
119
|
+
BucketEncryption: bucketEncryption,
|
|
120
|
+
PublicAccessBlockConfiguration: publicAccessBlock,
|
|
121
|
+
...(versioningConfiguration ? { VersioningConfiguration: versioningConfiguration } : {}),
|
|
122
|
+
...(websiteConfiguration ? { WebsiteConfiguration: websiteConfiguration } : {}),
|
|
123
|
+
Tags: props.tags,
|
|
124
|
+
},
|
|
125
|
+
defaults?.bucket,
|
|
126
|
+
),
|
|
127
|
+
{ DeletionPolicy: props.removalPolicy === "destroy" ? "Delete" : "Retain" },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const bucketPolicy = publicRead
|
|
131
|
+
? new S3BucketPolicy(
|
|
132
|
+
mergeDefaults(
|
|
133
|
+
{
|
|
134
|
+
Bucket: bucket.Ref,
|
|
135
|
+
PolicyDocument: {
|
|
136
|
+
Version: "2012-10-17",
|
|
137
|
+
Statement: [
|
|
138
|
+
{ Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: Sub`${bucket.Arn}/*` },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
defaults?.bucketPolicy,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
: undefined;
|
|
146
|
+
|
|
147
|
+
// Spread rather than a ternary between two object shapes (EksCluster's
|
|
148
|
+
// addon slots, ../eks-cluster.ts, hit the same union-return typing wall):
|
|
149
|
+
// a key that is sometimes present, never a key whose value is sometimes
|
|
150
|
+
// `undefined` — `CompositeMembers`' index signature requires `Declarable`,
|
|
151
|
+
// not `Declarable | undefined`.
|
|
152
|
+
return { bucket, ...(bucketPolicy ? { bucketPolicy } : {}) };
|
|
153
|
+
}, "BucketDeployment");
|
|
@@ -13,9 +13,12 @@ import { VpcDefault } from "./vpc-default";
|
|
|
13
13
|
import { FargateAlb } from "./fargate-alb";
|
|
14
14
|
import { AlbShared } from "./alb-shared";
|
|
15
15
|
import { FargateService } from "./fargate-service";
|
|
16
|
+
import { NlbService } from "./nlb-service";
|
|
16
17
|
import { RdsInstance } from "./rds-instance";
|
|
17
18
|
import { Ec2InstanceRole } from "./ec2-instance-role";
|
|
19
|
+
import { Ec2InstanceBundle } from "./ec2-instance-bundle";
|
|
18
20
|
import { MinimalVpc } from "./minimal-vpc";
|
|
21
|
+
import { StepFunctionsWorkflow } from "./step-functions-workflow";
|
|
19
22
|
|
|
20
23
|
const baseProps = {
|
|
21
24
|
name: "TestFunc",
|
|
@@ -639,6 +642,112 @@ describe("FargateService", () => {
|
|
|
639
642
|
});
|
|
640
643
|
});
|
|
641
644
|
|
|
645
|
+
describe("NlbService", () => {
|
|
646
|
+
const nlbProps = {
|
|
647
|
+
vpcId: "vpc-123",
|
|
648
|
+
subnetIds: ["subnet-pub1", "subnet-pub2"],
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
test("returns 3 members with correct names", () => {
|
|
652
|
+
const instance = NlbService(nlbProps);
|
|
653
|
+
const names = Object.keys(instance.members);
|
|
654
|
+
expect(names).toEqual(["nlb", "targetGroup", "listener"]);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("expandComposite produces correct logical names", () => {
|
|
658
|
+
const expanded = expandComposite("app", NlbService(nlbProps));
|
|
659
|
+
expect(expanded.has("appNlb")).toBe(true);
|
|
660
|
+
expect(expanded.has("appTargetGroup")).toBe(true);
|
|
661
|
+
expect(expanded.has("appListener")).toBe(true);
|
|
662
|
+
expect(expanded.size).toBe(3);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("NLB has Type network and given subnets", () => {
|
|
666
|
+
const instance = NlbService(nlbProps);
|
|
667
|
+
const nlbResourceProps = (instance.nlb as any).props;
|
|
668
|
+
expect(nlbResourceProps.Type).toBe("network");
|
|
669
|
+
expect(nlbResourceProps.Scheme).toBe("internet-facing");
|
|
670
|
+
expect(nlbResourceProps.Subnets).toEqual(["subnet-pub1", "subnet-pub2"]);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("no security group is created", () => {
|
|
674
|
+
const instance = NlbService(nlbProps);
|
|
675
|
+
expect(Object.keys(instance.members)).not.toContain("albSg");
|
|
676
|
+
expect(Object.keys(instance.members)).not.toContain("nlbSg");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("defaults: TCP protocol, port 80, ip target type", () => {
|
|
680
|
+
const instance = NlbService(nlbProps);
|
|
681
|
+
const tgProps = (instance.targetGroup as any).props;
|
|
682
|
+
expect(tgProps.Protocol).toBe("TCP");
|
|
683
|
+
expect(tgProps.Port).toBe(80);
|
|
684
|
+
expect(tgProps.TargetType).toBe("ip");
|
|
685
|
+
expect(tgProps.HealthCheckProtocol).toBe("TCP");
|
|
686
|
+
|
|
687
|
+
const listenerProps = (instance.listener as any).props;
|
|
688
|
+
expect(listenerProps.Protocol).toBe("TCP");
|
|
689
|
+
expect(listenerProps.Port).toBe(80);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("targetPort defaults to listenerPort but can be overridden", () => {
|
|
693
|
+
const instance = NlbService({ ...nlbProps, listenerPort: 443, targetPort: 8443 });
|
|
694
|
+
const tgProps = (instance.targetGroup as any).props;
|
|
695
|
+
expect(tgProps.Port).toBe(8443);
|
|
696
|
+
const listenerProps = (instance.listener as any).props;
|
|
697
|
+
expect(listenerProps.Port).toBe(443);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("listener forwards to the target group", () => {
|
|
701
|
+
const instance = NlbService(nlbProps);
|
|
702
|
+
const listenerProps = (instance.listener as any).props;
|
|
703
|
+
expect(listenerProps.DefaultActions).toHaveLength(1);
|
|
704
|
+
const action = (listenerProps.DefaultActions[0] as any).props;
|
|
705
|
+
expect(action.Type).toBe("forward");
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("static targets are registered on the target group", () => {
|
|
709
|
+
const instance = NlbService({
|
|
710
|
+
...nlbProps,
|
|
711
|
+
targetType: "instance",
|
|
712
|
+
targets: [{ id: "i-0123456789abcdef0", port: 8080 }],
|
|
713
|
+
});
|
|
714
|
+
const tgProps = (instance.targetGroup as any).props;
|
|
715
|
+
expect(tgProps.TargetType).toBe("instance");
|
|
716
|
+
expect(tgProps.Targets).toHaveLength(1);
|
|
717
|
+
const target = (tgProps.Targets[0] as any).props;
|
|
718
|
+
expect(target.Id).toBe("i-0123456789abcdef0");
|
|
719
|
+
expect(target.Port).toBe(8080);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("internal scheme is respected", () => {
|
|
723
|
+
const instance = NlbService({ ...nlbProps, scheme: "internal" });
|
|
724
|
+
const nlbResourceProps = (instance.nlb as any).props;
|
|
725
|
+
expect(nlbResourceProps.Scheme).toBe("internal");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("TLS protocol adds certificate to listener", () => {
|
|
729
|
+
const instance = NlbService({
|
|
730
|
+
...nlbProps,
|
|
731
|
+
protocol: "TLS",
|
|
732
|
+
certificateArn: "arn:aws:acm:us-east-1:123:certificate/abc",
|
|
733
|
+
});
|
|
734
|
+
const listenerProps = (instance.listener as any).props;
|
|
735
|
+
expect(listenerProps.Protocol).toBe("TLS");
|
|
736
|
+
expect(listenerProps.Certificates).toHaveLength(1);
|
|
737
|
+
const cert = (listenerProps.Certificates[0] as any).props;
|
|
738
|
+
expect(cert.CertificateArn).toBe("arn:aws:acm:us-east-1:123:certificate/abc");
|
|
739
|
+
|
|
740
|
+
const tgProps = (instance.targetGroup as any).props;
|
|
741
|
+
expect(tgProps.Protocol).toBe("TLS");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("throws when protocol is TLS without a certificateArn", () => {
|
|
745
|
+
expect(() =>
|
|
746
|
+
NlbService({ ...nlbProps, protocol: "TLS" }),
|
|
747
|
+
).toThrow("NlbService requires certificateArn when protocol is TLS");
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
642
751
|
describe("RdsInstance", () => {
|
|
643
752
|
const rdsProps = {
|
|
644
753
|
vpcId: "vpc-123",
|
|
@@ -897,6 +1006,140 @@ describe("Ec2InstanceRole", () => {
|
|
|
897
1006
|
});
|
|
898
1007
|
});
|
|
899
1008
|
|
|
1009
|
+
describe("Ec2InstanceBundle", () => {
|
|
1010
|
+
const instanceProps = {
|
|
1011
|
+
imageId: "ami-0abcdef1234567890",
|
|
1012
|
+
vpcId: "vpc-123",
|
|
1013
|
+
subnetId: "subnet-456",
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
test("returns role, instanceProfile, sg, instance members", () => {
|
|
1017
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1018
|
+
expect(Object.keys(instance.members)).toEqual(["role", "instanceProfile", "sg", "instance"]);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("expandComposite produces correct logical names", () => {
|
|
1022
|
+
const expanded = expandComposite("web", Ec2InstanceBundle(instanceProps));
|
|
1023
|
+
expect(expanded.has("webRole")).toBe(true);
|
|
1024
|
+
expect(expanded.has("webInstanceProfile")).toBe(true);
|
|
1025
|
+
expect(expanded.has("webSg")).toBe(true);
|
|
1026
|
+
expect(expanded.has("webInstance")).toBe(true);
|
|
1027
|
+
expect(expanded.size).toBe(4);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
test("role has EC2 trust policy", () => {
|
|
1031
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1032
|
+
const roleProps = (instance.role as any).props;
|
|
1033
|
+
expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("ec2.amazonaws.com");
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
test("instanceProfile references the role", () => {
|
|
1037
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1038
|
+
const profileProps = (instance.instanceProfile as any).props;
|
|
1039
|
+
expect(profileProps.Roles).toHaveLength(1);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test("ManagedPolicyArns and Policies pass through to the role", () => {
|
|
1043
|
+
const arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore";
|
|
1044
|
+
const instance = Ec2InstanceBundle({
|
|
1045
|
+
...instanceProps,
|
|
1046
|
+
ManagedPolicyArns: [arn],
|
|
1047
|
+
Policies: [{ PolicyName: "extra", PolicyDocument: { Version: "2012-10-17", Statement: [] } }],
|
|
1048
|
+
});
|
|
1049
|
+
const roleProps = (instance.role as any).props;
|
|
1050
|
+
expect(roleProps.ManagedPolicyArns).toContain(arn);
|
|
1051
|
+
expect(roleProps.Policies).toHaveLength(1);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test("instance wires ImageId, InstanceType default, subnet, SG, and instance profile", () => {
|
|
1055
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1056
|
+
const instanceProps_ = (instance.instance as any).props;
|
|
1057
|
+
expect(instanceProps_.ImageId).toBe("ami-0abcdef1234567890");
|
|
1058
|
+
expect(instanceProps_.InstanceType).toBe("t3.micro");
|
|
1059
|
+
expect(instanceProps_.SubnetId).toBe("subnet-456");
|
|
1060
|
+
expect(instanceProps_.SecurityGroupIds).toHaveLength(1);
|
|
1061
|
+
expect(instanceProps_.IamInstanceProfile).toBeDefined();
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
test("custom instanceType is respected", () => {
|
|
1065
|
+
const instance = Ec2InstanceBundle({ ...instanceProps, instanceType: "m5.large" });
|
|
1066
|
+
expect((instance.instance as any).props.InstanceType).toBe("m5.large");
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test("keyName is passed through when provided", () => {
|
|
1070
|
+
const instance = Ec2InstanceBundle({ ...instanceProps, keyName: "my-key" });
|
|
1071
|
+
expect((instance.instance as any).props.KeyName).toBe("my-key");
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
test("keyName is omitted by default", () => {
|
|
1075
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1076
|
+
expect((instance.instance as any).props.KeyName).toBeUndefined();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
test("userData is wrapped in Fn::Base64", () => {
|
|
1080
|
+
const instance = Ec2InstanceBundle({ ...instanceProps, userData: "#!/bin/bash\necho hi" });
|
|
1081
|
+
const userData = (instance.instance as any).props.UserData;
|
|
1082
|
+
expect(JSON.stringify(userData)).toContain("Fn::Base64");
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("no ingress rules by default", () => {
|
|
1086
|
+
const instance = Ec2InstanceBundle(instanceProps);
|
|
1087
|
+
expect((instance.sg as any).props.SecurityGroupIngress).toBeUndefined();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("ingress with cidr produces a CidrIp rule", () => {
|
|
1091
|
+
const instance = Ec2InstanceBundle({
|
|
1092
|
+
...instanceProps,
|
|
1093
|
+
ingress: [{ fromPort: 22, cidr: "10.0.0.0/16", description: "ssh" }],
|
|
1094
|
+
});
|
|
1095
|
+
const rules = (instance.sg as any).props.SecurityGroupIngress;
|
|
1096
|
+
expect(rules).toHaveLength(1);
|
|
1097
|
+
const rule = (rules[0] as any).props;
|
|
1098
|
+
expect(rule.CidrIp).toBe("10.0.0.0/16");
|
|
1099
|
+
expect(rule.FromPort).toBe(22);
|
|
1100
|
+
expect(rule.ToPort).toBe(22);
|
|
1101
|
+
expect(rule.IpProtocol).toBe("tcp");
|
|
1102
|
+
expect(rule.Description).toBe("ssh");
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
test("ingress with sourceSecurityGroupId produces a SG-sourced rule", () => {
|
|
1106
|
+
const instance = Ec2InstanceBundle({
|
|
1107
|
+
...instanceProps,
|
|
1108
|
+
ingress: [{ fromPort: 80, toPort: 8080, sourceSecurityGroupId: "sg-abc123" }],
|
|
1109
|
+
});
|
|
1110
|
+
const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
|
|
1111
|
+
expect(rule.SourceSecurityGroupId).toBe("sg-abc123");
|
|
1112
|
+
expect(rule.FromPort).toBe(80);
|
|
1113
|
+
expect(rule.ToPort).toBe(8080);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test("ingress with cidrIpv6 produces an IPv6 rule", () => {
|
|
1117
|
+
const instance = Ec2InstanceBundle({
|
|
1118
|
+
...instanceProps,
|
|
1119
|
+
ingress: [{ fromPort: 443, cidrIpv6: "::/0" }],
|
|
1120
|
+
});
|
|
1121
|
+
const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
|
|
1122
|
+
expect(rule.CidrIpv6).toBe("::/0");
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
test("ingress rule with no source throws", () => {
|
|
1126
|
+
expect(() =>
|
|
1127
|
+
Ec2InstanceBundle({ ...instanceProps, ingress: [{ fromPort: 22 }] }),
|
|
1128
|
+
).toThrow(/needs one of cidr, cidrIpv6, or sourceSecurityGroupId/);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
test("multiple ingress rules are all applied", () => {
|
|
1132
|
+
const instance = Ec2InstanceBundle({
|
|
1133
|
+
...instanceProps,
|
|
1134
|
+
ingress: [
|
|
1135
|
+
{ fromPort: 22, cidr: "10.0.0.0/16" },
|
|
1136
|
+
{ fromPort: 443, cidr: "0.0.0.0/0" },
|
|
1137
|
+
],
|
|
1138
|
+
});
|
|
1139
|
+
expect((instance.sg as any).props.SecurityGroupIngress).toHaveLength(2);
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
900
1143
|
describe("MinimalVpc", () => {
|
|
901
1144
|
test("returns 8 members", () => {
|
|
902
1145
|
const instance = MinimalVpc({});
|
|
@@ -930,3 +1173,85 @@ describe("MinimalVpc", () => {
|
|
|
930
1173
|
expect(expanded.size).toBe(8);
|
|
931
1174
|
});
|
|
932
1175
|
});
|
|
1176
|
+
|
|
1177
|
+
describe("StepFunctionsWorkflow", () => {
|
|
1178
|
+
const passDefinition = { StartAt: "Hello", States: { Hello: { Type: "Pass", End: true } } };
|
|
1179
|
+
|
|
1180
|
+
test("returns role, logGroup, stateMachine members", () => {
|
|
1181
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1182
|
+
expect(Object.keys(instance.members)).toEqual(["role", "logGroup", "stateMachine"]);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test("role has states.amazonaws.com trust policy", () => {
|
|
1186
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1187
|
+
const roleProps = (instance.role as any).props;
|
|
1188
|
+
expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("states.amazonaws.com");
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("role always carries the log-delivery policy", () => {
|
|
1192
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1193
|
+
const roleProps = (instance.role as any).props;
|
|
1194
|
+
const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
|
|
1195
|
+
expect(names).toContain("StepFunctionsLogDelivery");
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test("lambdaFunctionArns grants InvokeFunction on those ARNs", () => {
|
|
1199
|
+
const instance = StepFunctionsWorkflow({
|
|
1200
|
+
definition: passDefinition,
|
|
1201
|
+
lambdaFunctionArns: ["arn:aws:lambda:us-east-1:123456789012:function:worker"],
|
|
1202
|
+
});
|
|
1203
|
+
const roleProps = (instance.role as any).props;
|
|
1204
|
+
const invokePolicy = roleProps.Policies.find((p: any) => p.props.PolicyName === "InvokeLambdaTasks");
|
|
1205
|
+
expect(invokePolicy).toBeDefined();
|
|
1206
|
+
expect(invokePolicy.props.PolicyDocument.Statement[0].Resource).toEqual([
|
|
1207
|
+
"arn:aws:lambda:us-east-1:123456789012:function:worker",
|
|
1208
|
+
]);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test("without lambdaFunctionArns, no InvokeLambdaTasks policy", () => {
|
|
1212
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1213
|
+
const roleProps = (instance.role as any).props;
|
|
1214
|
+
const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
|
|
1215
|
+
expect(names).not.toContain("InvokeLambdaTasks");
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test("stateMachine carries the definition object as Definition (not a string)", () => {
|
|
1219
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1220
|
+
const smProps = (instance.stateMachine as any).props;
|
|
1221
|
+
expect(smProps.Definition).toEqual(passDefinition);
|
|
1222
|
+
expect(smProps.DefinitionString).toBeUndefined();
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
test("stateMachine references role.Arn and defaults to STANDARD", () => {
|
|
1226
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1227
|
+
const smProps = (instance.stateMachine as any).props;
|
|
1228
|
+
expect(smProps.RoleArn).toBeInstanceOf(AttrRef);
|
|
1229
|
+
expect(smProps.StateMachineType).toBe("STANDARD");
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
test("logGroup defaults to 14-day retention, overridable", () => {
|
|
1233
|
+
const defaultInstance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1234
|
+
expect((defaultInstance.logGroup as any).props.RetentionInDays).toBe(14);
|
|
1235
|
+
|
|
1236
|
+
const overridden = StepFunctionsWorkflow({ definition: passDefinition, logRetentionDays: 30 });
|
|
1237
|
+
expect((overridden.logGroup as any).props.RetentionInDays).toBe(30);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
test("LoggingConfiguration wires the log group and defaults level to ALL", () => {
|
|
1241
|
+
const instance = StepFunctionsWorkflow({ definition: passDefinition });
|
|
1242
|
+
const smProps = (instance.stateMachine as any).props;
|
|
1243
|
+
const loggingProps = (smProps.LoggingConfiguration as any).props;
|
|
1244
|
+
expect(loggingProps.Level).toBe("ALL");
|
|
1245
|
+
const destProps = (loggingProps.Destinations[0] as any).props;
|
|
1246
|
+
const cwProps = (destProps.CloudWatchLogsLogGroup as any).props;
|
|
1247
|
+
expect(cwProps.LogGroupArn).toBeInstanceOf(AttrRef);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test("expandComposite produces 3 entries", () => {
|
|
1251
|
+
const expanded = expandComposite("workflow", StepFunctionsWorkflow({ definition: passDefinition }));
|
|
1252
|
+
expect(expanded.has("workflowRole")).toBe(true);
|
|
1253
|
+
expect(expanded.has("workflowLogGroup")).toBe(true);
|
|
1254
|
+
expect(expanded.has("workflowStateMachine")).toBe(true);
|
|
1255
|
+
expect(expanded.size).toBe(3);
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { expandComposite } from "@intentius/chant";
|
|
3
|
+
import { DynamoDBTable } from "./dynamodb-table";
|
|
4
|
+
|
|
5
|
+
// Generated property-kind instances (Table_KeySchema, Table_Projection, ...) store
|
|
6
|
+
// their data on a non-enumerable `.props`, so `toEqual` against a plain object
|
|
7
|
+
// compares empty own-enumerable-property sets unless each instance is unwrapped
|
|
8
|
+
// first. `p` unwraps one instance; `ps` maps an array of them.
|
|
9
|
+
const p = (x: any) => x.props;
|
|
10
|
+
const ps = (xs: any[]) => xs.map(p);
|
|
11
|
+
|
|
12
|
+
describe("DynamoDBTable", () => {
|
|
13
|
+
test("partition-key-only: PAY_PER_REQUEST by default, single member", () => {
|
|
14
|
+
const instance = DynamoDBTable({ partitionKey: { name: "id" } });
|
|
15
|
+
expect(Object.keys(instance.members)).toEqual(["table"]);
|
|
16
|
+
const props = (instance.table as any).props;
|
|
17
|
+
expect(props.BillingMode).toBe("PAY_PER_REQUEST");
|
|
18
|
+
expect(ps(props.KeySchema)).toEqual([{ AttributeName: "id", KeyType: "HASH" }]);
|
|
19
|
+
expect(ps(props.AttributeDefinitions)).toEqual([{ AttributeName: "id", AttributeType: "S" }]);
|
|
20
|
+
expect(props.ProvisionedThroughput).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("expandComposite produces the table's logical name", () => {
|
|
24
|
+
const expanded = expandComposite("orders", DynamoDBTable({ partitionKey: { name: "id" } }));
|
|
25
|
+
expect(expanded.has("ordersTable")).toBe(true);
|
|
26
|
+
expect(expanded.size).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("partition + sort key, non-default types", () => {
|
|
30
|
+
const instance = DynamoDBTable({
|
|
31
|
+
partitionKey: { name: "pk", type: "S" },
|
|
32
|
+
sortKey: { name: "sk", type: "N" },
|
|
33
|
+
});
|
|
34
|
+
const props = (instance.table as any).props;
|
|
35
|
+
expect(ps(props.KeySchema)).toEqual([
|
|
36
|
+
{ AttributeName: "pk", KeyType: "HASH" },
|
|
37
|
+
{ AttributeName: "sk", KeyType: "RANGE" },
|
|
38
|
+
]);
|
|
39
|
+
expect(ps(props.AttributeDefinitions)).toEqual([
|
|
40
|
+
{ AttributeName: "pk", AttributeType: "S" },
|
|
41
|
+
{ AttributeName: "sk", AttributeType: "N" },
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("no GSIs: GlobalSecondaryIndexes is omitted, not an empty array", () => {
|
|
46
|
+
const props = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
|
|
47
|
+
expect(props.GlobalSecondaryIndexes).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a GSI defaults to ALL projection and, on-demand billing, carries no throughput", () => {
|
|
51
|
+
const instance = DynamoDBTable({
|
|
52
|
+
partitionKey: { name: "pk" },
|
|
53
|
+
globalSecondaryIndexes: [{ name: "byStatus", partitionKey: { name: "status" } }],
|
|
54
|
+
});
|
|
55
|
+
const props = (instance.table as any).props;
|
|
56
|
+
expect(props.GlobalSecondaryIndexes).toHaveLength(1);
|
|
57
|
+
const gsi = p(props.GlobalSecondaryIndexes[0]);
|
|
58
|
+
expect(gsi.IndexName).toBe("byStatus");
|
|
59
|
+
expect(ps(gsi.KeySchema)).toEqual([{ AttributeName: "status", KeyType: "HASH" }]);
|
|
60
|
+
expect(p(gsi.Projection)).toEqual({ ProjectionType: "ALL" });
|
|
61
|
+
expect(gsi.ProvisionedThroughput).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a GSI reusing the table's partition key does not duplicate its AttributeDefinition", () => {
|
|
65
|
+
const instance = DynamoDBTable({
|
|
66
|
+
partitionKey: { name: "pk" },
|
|
67
|
+
sortKey: { name: "sk" },
|
|
68
|
+
globalSecondaryIndexes: [{ name: "byPk", partitionKey: { name: "pk" }, sortKey: { name: "gsiSort" } }],
|
|
69
|
+
});
|
|
70
|
+
const props = (instance.table as any).props;
|
|
71
|
+
const names = ps(props.AttributeDefinitions).map((a: any) => a.AttributeName);
|
|
72
|
+
expect(names).toEqual(["pk", "sk", "gsiSort"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("INCLUDE projection carries nonKeyAttributes through", () => {
|
|
76
|
+
const instance = DynamoDBTable({
|
|
77
|
+
partitionKey: { name: "pk" },
|
|
78
|
+
globalSecondaryIndexes: [{
|
|
79
|
+
name: "byStatus",
|
|
80
|
+
partitionKey: { name: "status" },
|
|
81
|
+
projectionType: "INCLUDE",
|
|
82
|
+
nonKeyAttributes: ["total", "updatedAt"],
|
|
83
|
+
}],
|
|
84
|
+
});
|
|
85
|
+
const gsi = p(((instance.table as any).props.GlobalSecondaryIndexes)[0]);
|
|
86
|
+
expect(p(gsi.Projection)).toEqual({ ProjectionType: "INCLUDE", NonKeyAttributes: ["total", "updatedAt"] });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("ttlAttribute enables TimeToLiveSpecification; omitted, no TTL block at all", () => {
|
|
90
|
+
const withTtl = (DynamoDBTable({ partitionKey: { name: "id" }, ttlAttribute: "expiresAt" }).table as any).props;
|
|
91
|
+
expect(p(withTtl.TimeToLiveSpecification)).toEqual({ Enabled: true, AttributeName: "expiresAt" });
|
|
92
|
+
|
|
93
|
+
const withoutTtl = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
|
|
94
|
+
expect(withoutTtl.TimeToLiveSpecification).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("stream: true defaults to NEW_AND_OLD_IMAGES", () => {
|
|
98
|
+
const props = (DynamoDBTable({ partitionKey: { name: "id" }, stream: true }).table as any).props;
|
|
99
|
+
expect(p(props.StreamSpecification)).toEqual({ StreamViewType: "NEW_AND_OLD_IMAGES" });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("stream view type is overridable; omitted, no stream block at all", () => {
|
|
103
|
+
const withView = (DynamoDBTable({
|
|
104
|
+
partitionKey: { name: "id" },
|
|
105
|
+
stream: { viewType: "KEYS_ONLY" },
|
|
106
|
+
}).table as any).props;
|
|
107
|
+
expect(p(withView.StreamSpecification)).toEqual({ StreamViewType: "KEYS_ONLY" });
|
|
108
|
+
|
|
109
|
+
const withoutStream = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
|
|
110
|
+
expect(withoutStream.StreamSpecification).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("PROVISIONED billing mode defaults capacity to 5/5 on both table and GSI", () => {
|
|
114
|
+
const instance = DynamoDBTable({
|
|
115
|
+
partitionKey: { name: "id" },
|
|
116
|
+
billingMode: "PROVISIONED",
|
|
117
|
+
globalSecondaryIndexes: [{ name: "byStatus", partitionKey: { name: "status" } }],
|
|
118
|
+
});
|
|
119
|
+
const props = (instance.table as any).props;
|
|
120
|
+
expect(p(props.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 5, WriteCapacityUnits: 5 });
|
|
121
|
+
const gsi = p(props.GlobalSecondaryIndexes[0]);
|
|
122
|
+
expect(p(gsi.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 5, WriteCapacityUnits: 5 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("PROVISIONED billing mode: explicit table capacity flows to a GSI that doesn't set its own", () => {
|
|
126
|
+
const instance = DynamoDBTable({
|
|
127
|
+
partitionKey: { name: "id" },
|
|
128
|
+
billingMode: "PROVISIONED",
|
|
129
|
+
provisionedThroughput: { readCapacity: 20, writeCapacity: 10 },
|
|
130
|
+
globalSecondaryIndexes: [
|
|
131
|
+
{ name: "inherits", partitionKey: { name: "status" } },
|
|
132
|
+
{ name: "overrides", partitionKey: { name: "kind" }, provisionedThroughput: { readCapacity: 1, writeCapacity: 1 } },
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
const props = (instance.table as any).props;
|
|
136
|
+
expect(p(props.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 20, WriteCapacityUnits: 10 });
|
|
137
|
+
const [inherits, overrides] = ps(props.GlobalSecondaryIndexes);
|
|
138
|
+
expect(p(inherits.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 20, WriteCapacityUnits: 10 });
|
|
139
|
+
expect(p(overrides.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 1, WriteCapacityUnits: 1 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("tableName is threaded through when given", () => {
|
|
143
|
+
const props = (DynamoDBTable({ partitionKey: { name: "id" }, tableName: "orders-prod" }).table as any).props;
|
|
144
|
+
expect(props.TableName).toBe("orders-prod");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("defaults escape hatch reaches the table (e.g. deletion protection)", () => {
|
|
148
|
+
const props = (DynamoDBTable({
|
|
149
|
+
partitionKey: { name: "id" },
|
|
150
|
+
defaults: { table: { DeletionProtectionEnabled: true } },
|
|
151
|
+
}).table as any).props;
|
|
152
|
+
expect(props.DeletionProtectionEnabled).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|