alchemy 0.53.0 → 0.55.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/bin/alchemy.js +196353 -0
- package/bin/alchemy.ts +2 -2
- package/bin/commands/init.ts +1190 -0
- package/bin/constants.ts +18 -0
- package/bin/services/execute-alchemy.ts +7 -2
- package/bin/services/package-manager.ts +1 -1
- package/bin/types.ts +14 -5
- package/lib/aws/ec2/index.d.ts +10 -0
- package/lib/aws/ec2/index.d.ts.map +1 -0
- package/lib/aws/ec2/index.js +10 -0
- package/lib/aws/ec2/index.js.map +1 -0
- package/lib/aws/ec2/internet-gateway-attachment.d.ts +150 -0
- package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -0
- package/lib/aws/ec2/internet-gateway-attachment.js +248 -0
- package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -0
- package/lib/aws/ec2/internet-gateway.d.ts +132 -0
- package/lib/aws/ec2/internet-gateway.d.ts.map +1 -0
- package/lib/aws/ec2/internet-gateway.js +311 -0
- package/lib/aws/ec2/internet-gateway.js.map +1 -0
- package/lib/aws/ec2/nat-gateway.d.ts +145 -0
- package/lib/aws/ec2/nat-gateway.d.ts.map +1 -0
- package/lib/aws/ec2/nat-gateway.js +618 -0
- package/lib/aws/ec2/nat-gateway.js.map +1 -0
- package/lib/aws/ec2/route-table-association.d.ts +134 -0
- package/lib/aws/ec2/route-table-association.d.ts.map +1 -0
- package/lib/aws/ec2/route-table-association.js +364 -0
- package/lib/aws/ec2/route-table-association.js.map +1 -0
- package/lib/aws/ec2/route-table.d.ts +94 -0
- package/lib/aws/ec2/route-table.d.ts.map +1 -0
- package/lib/aws/ec2/route-table.js +253 -0
- package/lib/aws/ec2/route-table.js.map +1 -0
- package/lib/aws/ec2/route.d.ts +130 -0
- package/lib/aws/ec2/route.d.ts.map +1 -0
- package/lib/aws/ec2/route.js +253 -0
- package/lib/aws/ec2/route.js.map +1 -0
- package/lib/aws/ec2/security-group-rule.d.ts +84 -0
- package/lib/aws/ec2/security-group-rule.d.ts.map +1 -0
- package/lib/aws/ec2/security-group-rule.js +123 -0
- package/lib/aws/ec2/security-group-rule.js.map +1 -0
- package/lib/aws/ec2/security-group.d.ts +84 -0
- package/lib/aws/ec2/security-group.d.ts.map +1 -0
- package/lib/aws/ec2/security-group.js +207 -0
- package/lib/aws/ec2/security-group.js.map +1 -0
- package/lib/aws/ec2/subnet.d.ts +110 -0
- package/lib/aws/ec2/subnet.d.ts.map +1 -0
- package/lib/aws/ec2/subnet.js +336 -0
- package/lib/aws/ec2/subnet.js.map +1 -0
- package/lib/aws/ec2/utils.d.ts +15 -0
- package/lib/aws/ec2/utils.d.ts.map +1 -0
- package/lib/aws/ec2/utils.js +79 -0
- package/lib/aws/ec2/utils.js.map +1 -0
- package/lib/aws/ec2/vpc.d.ts +160 -0
- package/lib/aws/ec2/vpc.d.ts.map +1 -0
- package/lib/aws/ec2/vpc.js +453 -0
- package/lib/aws/ec2/vpc.js.map +1 -0
- package/lib/aws/index.d.ts +7 -0
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +8 -0
- package/lib/aws/index.js.map +1 -1
- package/lib/aws/utils.d.ts +25 -0
- package/lib/aws/utils.d.ts.map +1 -0
- package/lib/aws/utils.js +89 -0
- package/lib/aws/utils.js.map +1 -0
- package/lib/build-date.d.ts +6 -0
- package/lib/build-date.d.ts.map +1 -0
- package/lib/build-date.js +8 -0
- package/lib/build-date.js.map +1 -0
- package/lib/cloudflare/astro.d.ts.map +1 -1
- package/lib/cloudflare/astro.js +4 -3
- package/lib/cloudflare/astro.js.map +1 -1
- package/lib/cloudflare/bundle/externals.d.ts +3 -0
- package/lib/cloudflare/bundle/externals.d.ts.map +1 -0
- package/lib/cloudflare/bundle/externals.js +35 -0
- package/lib/cloudflare/bundle/externals.js.map +1 -0
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +8 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.js +11 -4
- package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.d.ts +4 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.js +27 -11
- package/lib/cloudflare/bundle/plugin-hot-reload.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +5 -1
- package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-wasm.d.ts +6 -2
- package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-wasm.js +21 -47
- package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
- package/lib/cloudflare/bundle/validate-node-compat.d.ts +2 -1
- package/lib/cloudflare/bundle/validate-node-compat.d.ts.map +1 -1
- package/lib/cloudflare/bundle/validate-node-compat.js +5 -4
- package/lib/cloudflare/bundle/validate-node-compat.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/d1-database.js +1 -1
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-migrations.js +8 -10
- package/lib/cloudflare/d1-migrations.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +3 -3
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-options.js +23 -6
- package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare.js +2 -4
- package/lib/cloudflare/miniflare/miniflare.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +4 -6
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/react-router.d.ts +4 -0
- package/lib/cloudflare/react-router.d.ts.map +1 -1
- package/lib/cloudflare/react-router.js +8 -5
- package/lib/cloudflare/react-router.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts +80 -0
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -0
- package/lib/cloudflare/worker-bundle.js +266 -0
- package/lib/cloudflare/worker-bundle.js.map +1 -0
- package/lib/cloudflare/worker.d.ts +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +81 -195
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/esbuild/bundle.d.ts +1 -0
- package/lib/esbuild/bundle.d.ts.map +1 -1
- package/lib/state/cloudflare-state-store.js +1 -5
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/state/dofs-state-store/store.d.ts.map +1 -1
- package/lib/state/dofs-state-store/store.js +4 -4
- package/lib/state/dofs-state-store/store.js.map +1 -1
- package/lib/util/params.d.ts +5 -0
- package/lib/util/params.d.ts.map +1 -0
- package/lib/util/params.js +30 -0
- package/lib/util/params.js.map +1 -0
- package/lib/util/timeout.d.ts +31 -0
- package/lib/util/timeout.d.ts.map +1 -0
- package/lib/util/timeout.js +109 -0
- package/lib/util/timeout.js.map +1 -0
- package/lib/util/xml.d.ts +5 -0
- package/lib/util/xml.d.ts.map +1 -0
- package/lib/util/xml.js +8 -0
- package/lib/util/xml.js.map +1 -0
- package/package.json +12 -5
- package/src/aws/ec2/index.ts +9 -0
- package/src/aws/ec2/internet-gateway-attachment.ts +468 -0
- package/src/aws/ec2/internet-gateway.ts +557 -0
- package/src/aws/ec2/nat-gateway.ts +1123 -0
- package/src/aws/ec2/route-table-association.ts +747 -0
- package/src/aws/ec2/route-table.ts +452 -0
- package/src/aws/ec2/route.ts +447 -0
- package/src/aws/ec2/security-group-rule.ts +229 -0
- package/src/aws/ec2/security-group.ts +418 -0
- package/src/aws/ec2/subnet.ts +625 -0
- package/src/aws/ec2/utils.ts +102 -0
- package/src/aws/ec2/vpc.ts +969 -0
- package/src/aws/index.ts +9 -0
- package/src/aws/utils.ts +137 -0
- package/src/build-date.ts +8 -0
- package/src/cloudflare/astro.ts +4 -3
- package/src/cloudflare/bundle/externals.ts +36 -0
- package/src/cloudflare/bundle/internal-worker-bundle.ts +12 -3
- package/src/cloudflare/bundle/plugin-hot-reload.ts +32 -12
- package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +6 -1
- package/src/cloudflare/bundle/plugin-wasm.ts +26 -54
- package/src/cloudflare/bundle/validate-node-compat.ts +6 -5
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-database.ts +1 -1
- package/src/cloudflare/d1-migrations.ts +19 -18
- package/src/cloudflare/miniflare/miniflare-worker-options.ts +27 -13
- package/src/cloudflare/miniflare/miniflare.ts +2 -4
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +5 -7
- package/src/cloudflare/react-router.ts +14 -6
- package/src/cloudflare/worker-bundle.ts +364 -0
- package/src/cloudflare/worker.ts +104 -241
- package/src/state/cloudflare-state-store.ts +1 -5
- package/src/state/dofs-state-store/store.ts +4 -4
- package/src/util/params.ts +33 -0
- package/src/util/timeout.ts +178 -0
- package/src/util/xml.ts +7 -0
- package/templates/nuxt/package.json +1 -1
- package/templates/nuxt/types/env.d.ts +11 -0
- package/templates/react-router/workers/app.ts +2 -2
- package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +385 -0
- package/templates/rwsdk/wrangler.jsonc +10 -7
- package/templates/typescript/tsconfig.json +1 -2
- package/bin/alchemy.mjs +0 -51766
- package/lib/cloudflare/bundle/esbuild.d.ts +0 -16
- package/lib/cloudflare/bundle/esbuild.d.ts.map +0 -1
- package/lib/cloudflare/bundle/esbuild.js +0 -159
- package/lib/cloudflare/bundle/esbuild.js.map +0 -1
- package/lib/cloudflare/bundle/fs.d.ts +0 -18
- package/lib/cloudflare/bundle/fs.d.ts.map +0 -1
- package/lib/cloudflare/bundle/fs.js +0 -78
- package/lib/cloudflare/bundle/fs.js.map +0 -1
- package/lib/cloudflare/bundle/index.d.ts +0 -3
- package/lib/cloudflare/bundle/index.d.ts.map +0 -1
- package/lib/cloudflare/bundle/index.js +0 -2
- package/lib/cloudflare/bundle/index.js.map +0 -1
- package/lib/cloudflare/bundle/inline.d.ts +0 -12
- package/lib/cloudflare/bundle/inline.d.ts.map +0 -1
- package/lib/cloudflare/bundle/inline.js +0 -32
- package/lib/cloudflare/bundle/inline.js.map +0 -1
- package/lib/cloudflare/bundle/normalize.d.ts +0 -21
- package/lib/cloudflare/bundle/normalize.d.ts.map +0 -1
- package/lib/cloudflare/bundle/normalize.js +0 -49
- package/lib/cloudflare/bundle/normalize.js.map +0 -1
- package/lib/cloudflare/bundle/shared.d.ts +0 -30
- package/lib/cloudflare/bundle/shared.d.ts.map +0 -1
- package/lib/cloudflare/bundle/shared.js +0 -34
- package/lib/cloudflare/bundle/shared.js.map +0 -1
- package/src/cloudflare/bundle/esbuild.ts +0 -194
- package/src/cloudflare/bundle/fs.ts +0 -103
- package/src/cloudflare/bundle/index.ts +0 -6
- package/src/cloudflare/bundle/inline.ts +0 -42
- package/src/cloudflare/bundle/normalize.ts +0 -76
- package/src/cloudflare/bundle/shared.ts +0 -73
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import type { AwsClient } from "aws4fetch";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
import type { Context } from "../../context.ts";
|
|
4
|
+
import { Resource } from "../../resource.ts";
|
|
5
|
+
import { ignore } from "../../util/ignore.ts";
|
|
6
|
+
import { logger } from "../../util/logger.ts";
|
|
7
|
+
import {
|
|
8
|
+
mergeTimeoutConfig,
|
|
9
|
+
type TimeoutConfig,
|
|
10
|
+
waitForResourceState,
|
|
11
|
+
} from "../../util/timeout.ts";
|
|
12
|
+
import type { RouteTable } from "./route-table.ts";
|
|
13
|
+
import type { Subnet } from "./subnet.ts";
|
|
14
|
+
import { callEC2Api, createEC2Client } from "./utils.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Properties for creating or updating a Route Table Association
|
|
18
|
+
*/
|
|
19
|
+
export interface RouteTableAssociationProps {
|
|
20
|
+
/**
|
|
21
|
+
* The route table to associate
|
|
22
|
+
*/
|
|
23
|
+
routeTable: RouteTable | string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The subnet to associate with the route table
|
|
27
|
+
* Either subnet or gateway must be specified, but not both
|
|
28
|
+
*/
|
|
29
|
+
subnet?: Subnet | string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The internet gateway or virtual private gateway to associate with the route table
|
|
33
|
+
* Either subnet or gateway must be specified, but not both
|
|
34
|
+
*/
|
|
35
|
+
gateway?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Timeout configuration for Route Table Association operations
|
|
39
|
+
* @default Route Table Association-specific sensible defaults (30 attempts, 1000ms delay)
|
|
40
|
+
*/
|
|
41
|
+
timeout?: Partial<TimeoutConfig>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Output returned after Route Table Association creation/update
|
|
46
|
+
*/
|
|
47
|
+
export interface RouteTableAssociation
|
|
48
|
+
extends Resource<"aws::RouteTableAssociation">,
|
|
49
|
+
RouteTableAssociationProps {
|
|
50
|
+
/**
|
|
51
|
+
* The ID of the route table association
|
|
52
|
+
*/
|
|
53
|
+
associationId: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The ID of the route table
|
|
57
|
+
*/
|
|
58
|
+
routeTableId: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The ID of the subnet (if associated with a subnet)
|
|
62
|
+
*/
|
|
63
|
+
subnetId?: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The ID of the gateway (if associated with a gateway)
|
|
67
|
+
*/
|
|
68
|
+
gatewayId?: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether this is the main route table association
|
|
72
|
+
*/
|
|
73
|
+
isMain: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The state of the association
|
|
77
|
+
*/
|
|
78
|
+
state: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wait for Route Table Association to reach a specific state
|
|
83
|
+
*/
|
|
84
|
+
async function waitForRouteTableAssociationState(
|
|
85
|
+
client: AwsClient,
|
|
86
|
+
routeTableId: string,
|
|
87
|
+
associationId: string,
|
|
88
|
+
targetState: string,
|
|
89
|
+
timeoutConfig: TimeoutConfig,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const checkFunction = async (): Promise<
|
|
92
|
+
AwsRouteTableAssociation | undefined
|
|
93
|
+
> => {
|
|
94
|
+
const response = await callEC2Api<DescribeRouteTablesResponse>(
|
|
95
|
+
client,
|
|
96
|
+
"DescribeRouteTables",
|
|
97
|
+
parseRouteTableAssociationXmlResponse,
|
|
98
|
+
convertDescribeRouteTablesParamsToAwsFormat({
|
|
99
|
+
RouteTableIds: [routeTableId],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const routeTable = response.RouteTables?.[0];
|
|
104
|
+
if (!routeTable) {
|
|
105
|
+
throw new Error(`Route table ${routeTableId} not found`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const association = routeTable.Associations?.find(
|
|
109
|
+
(a) => a.RouteTableAssociationId === associationId,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return association;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const isReady = (
|
|
116
|
+
association: AwsRouteTableAssociation | undefined,
|
|
117
|
+
): boolean => {
|
|
118
|
+
return association?.AssociationState?.State === targetState;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await waitForResourceState(
|
|
122
|
+
checkFunction,
|
|
123
|
+
isReady,
|
|
124
|
+
timeoutConfig,
|
|
125
|
+
associationId,
|
|
126
|
+
"Route Table Association",
|
|
127
|
+
`reach state ${targetState}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Wait for Route Table Association to be deleted
|
|
133
|
+
*/
|
|
134
|
+
async function waitForRouteTableAssociationDeleted(
|
|
135
|
+
client: AwsClient,
|
|
136
|
+
routeTableId: string,
|
|
137
|
+
associationId: string,
|
|
138
|
+
timeoutConfig: TimeoutConfig,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const checkFunction = async (): Promise<
|
|
141
|
+
AwsRouteTableAssociation | undefined
|
|
142
|
+
> => {
|
|
143
|
+
try {
|
|
144
|
+
const response = await callEC2Api<DescribeRouteTablesResponse>(
|
|
145
|
+
client,
|
|
146
|
+
"DescribeRouteTables",
|
|
147
|
+
parseRouteTableAssociationXmlResponse,
|
|
148
|
+
convertDescribeRouteTablesParamsToAwsFormat({
|
|
149
|
+
RouteTableIds: [routeTableId],
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const routeTable = response.RouteTables?.[0];
|
|
154
|
+
if (!routeTable) {
|
|
155
|
+
// Route table doesn't exist, so association is definitely gone
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const association = routeTable.Associations?.find(
|
|
160
|
+
(a) => a.RouteTableAssociationId === associationId,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return association;
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
// If Route Table is not found, association is definitely gone
|
|
166
|
+
if (
|
|
167
|
+
error.code === "InvalidRouteTableID.NotFound" ||
|
|
168
|
+
error.code === "InvalidRouteTable.NotFound"
|
|
169
|
+
) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const isReady = (
|
|
177
|
+
association: AwsRouteTableAssociation | undefined,
|
|
178
|
+
): boolean => {
|
|
179
|
+
// Association is deleted if it doesn't exist
|
|
180
|
+
return !association;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await waitForResourceState(
|
|
184
|
+
checkFunction,
|
|
185
|
+
isReady,
|
|
186
|
+
timeoutConfig,
|
|
187
|
+
associationId,
|
|
188
|
+
"Route Table Association",
|
|
189
|
+
"be deleted",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* AWS Route Table Association Resource
|
|
195
|
+
*
|
|
196
|
+
* Creates and manages associations between route tables and subnets or gateways.
|
|
197
|
+
* Each subnet can only be associated with one route table at a time. Route table
|
|
198
|
+
* associations determine which route table controls the traffic for a subnet.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Associate a subnet with a route table
|
|
202
|
+
* const association = await RouteTableAssociation("subnet-association", {
|
|
203
|
+
* routeTable: myRouteTable,
|
|
204
|
+
* subnet: mySubnet
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* // Associate a subnet with a route table using IDs
|
|
209
|
+
* const association = await RouteTableAssociation("subnet-association", {
|
|
210
|
+
* routeTable: "rtb-12345678",
|
|
211
|
+
* subnet: "subnet-12345678"
|
|
212
|
+
* });
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* // Associate a gateway with a route table
|
|
216
|
+
* const gatewayAssociation = await RouteTableAssociation("gateway-association", {
|
|
217
|
+
* routeTable: myRouteTable,
|
|
218
|
+
* gateway: "igw-12345678"
|
|
219
|
+
* });
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* // Create association with custom timeout configuration
|
|
223
|
+
* const customAssociation = await RouteTableAssociation("custom-association", {
|
|
224
|
+
* routeTable: myRouteTable,
|
|
225
|
+
* subnet: mySubnet,
|
|
226
|
+
* timeout: {
|
|
227
|
+
* maxAttempts: 60,
|
|
228
|
+
* delayMs: 2000
|
|
229
|
+
* }
|
|
230
|
+
* });
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* // Associate multiple subnets with the same route table
|
|
234
|
+
* const publicSubnets = [subnet1, subnet2, subnet3];
|
|
235
|
+
* const associations = await Promise.all(
|
|
236
|
+
* publicSubnets.map((subnet, index) =>
|
|
237
|
+
* RouteTableAssociation(`public-association-${index}`, {
|
|
238
|
+
* routeTable: publicRouteTable,
|
|
239
|
+
* subnet
|
|
240
|
+
* })
|
|
241
|
+
* )
|
|
242
|
+
* );
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* // Associate private subnets with a private route table
|
|
246
|
+
* const privateAssociation1 = await RouteTableAssociation("private-association-1", {
|
|
247
|
+
* routeTable: privateRouteTable,
|
|
248
|
+
* subnet: privateSubnet1
|
|
249
|
+
* });
|
|
250
|
+
*
|
|
251
|
+
* const privateAssociation2 = await RouteTableAssociation("private-association-2", {
|
|
252
|
+
* routeTable: privateRouteTable,
|
|
253
|
+
* subnet: privateSubnet2
|
|
254
|
+
* });
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // Handle association replacement scenarios
|
|
258
|
+
* const newAssociation = await RouteTableAssociation("replaced-association", {
|
|
259
|
+
* routeTable: newRouteTable,
|
|
260
|
+
* subnet: existingSubnet // This will replace any existing association
|
|
261
|
+
* });
|
|
262
|
+
*/
|
|
263
|
+
export const RouteTableAssociation = Resource(
|
|
264
|
+
"aws::RouteTableAssociation",
|
|
265
|
+
async function (
|
|
266
|
+
this: Context<RouteTableAssociation>,
|
|
267
|
+
_id: string,
|
|
268
|
+
props: RouteTableAssociationProps,
|
|
269
|
+
): Promise<RouteTableAssociation> {
|
|
270
|
+
const client = await createEC2Client();
|
|
271
|
+
const timeoutConfig = mergeTimeoutConfig(
|
|
272
|
+
ROUTE_TABLE_ASSOCIATION_TIMEOUT,
|
|
273
|
+
props.timeout,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const routeTableId =
|
|
277
|
+
typeof props.routeTable === "string"
|
|
278
|
+
? props.routeTable
|
|
279
|
+
: props.routeTable.routeTableId;
|
|
280
|
+
|
|
281
|
+
const subnetId = props.subnet
|
|
282
|
+
? typeof props.subnet === "string"
|
|
283
|
+
? props.subnet
|
|
284
|
+
: props.subnet.subnetId
|
|
285
|
+
: undefined;
|
|
286
|
+
|
|
287
|
+
const gatewayId = props.gateway;
|
|
288
|
+
|
|
289
|
+
// Validate that exactly one of subnet or gateway is specified
|
|
290
|
+
if ((!subnetId && !gatewayId) || (subnetId && gatewayId)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"Exactly one of subnet or gateway must be specified for route table association",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.phase === "delete") {
|
|
297
|
+
if (this.output?.associationId) {
|
|
298
|
+
logger.log(
|
|
299
|
+
`🗑️ Deleting Route Table Association: ${this.output.associationId}`,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await ignore("InvalidAssociationID.NotFound", async () => {
|
|
303
|
+
const params = convertDisassociateRouteTableParamsToAwsFormat({
|
|
304
|
+
AssociationId: this.output!.associationId,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await callEC2Api<DisassociateRouteTableResponse>(
|
|
308
|
+
client,
|
|
309
|
+
"DisassociateRouteTable",
|
|
310
|
+
parseRouteTableAssociationXmlResponse,
|
|
311
|
+
params,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Wait for association to be fully deleted
|
|
316
|
+
await waitForRouteTableAssociationDeleted(
|
|
317
|
+
client,
|
|
318
|
+
routeTableId,
|
|
319
|
+
this.output.associationId,
|
|
320
|
+
timeoutConfig,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
logger.log(
|
|
324
|
+
` ✅ Route Table Association ${this.output.associationId} deletion completed`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return this.destroy();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let associationId: string;
|
|
331
|
+
let isMain = false;
|
|
332
|
+
let state = "associated";
|
|
333
|
+
|
|
334
|
+
if (this.phase === "update" && this.output?.associationId) {
|
|
335
|
+
// For updates, we need to check if the route table or target has changed
|
|
336
|
+
const currentRouteTableId = this.output.routeTableId;
|
|
337
|
+
const currentSubnetId = this.output.subnetId;
|
|
338
|
+
const currentGatewayId = this.output.gatewayId;
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
currentRouteTableId !== routeTableId ||
|
|
342
|
+
currentSubnetId !== subnetId ||
|
|
343
|
+
currentGatewayId !== gatewayId
|
|
344
|
+
) {
|
|
345
|
+
// Route table or target has changed, we need to replace the association
|
|
346
|
+
const response = await callEC2Api<ReplaceRouteTableAssociationResponse>(
|
|
347
|
+
client,
|
|
348
|
+
"ReplaceRouteTableAssociation",
|
|
349
|
+
parseRouteTableAssociationXmlResponse,
|
|
350
|
+
{
|
|
351
|
+
AssociationId: this.output.associationId,
|
|
352
|
+
RouteTableId: routeTableId,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
associationId = response.NewAssociationId;
|
|
357
|
+
state = response.AssociationState?.State || "associated";
|
|
358
|
+
|
|
359
|
+
// Wait for the new association to be active
|
|
360
|
+
await waitForRouteTableAssociationState(
|
|
361
|
+
client,
|
|
362
|
+
routeTableId,
|
|
363
|
+
associationId,
|
|
364
|
+
"associated",
|
|
365
|
+
timeoutConfig,
|
|
366
|
+
);
|
|
367
|
+
} else {
|
|
368
|
+
// No changes needed, keep existing association
|
|
369
|
+
associationId = this.output.associationId;
|
|
370
|
+
state = this.output.state;
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// Create new association
|
|
374
|
+
const params = convertAssociateRouteTableParamsToAwsFormat({
|
|
375
|
+
RouteTableId: routeTableId,
|
|
376
|
+
SubnetId: subnetId,
|
|
377
|
+
GatewayId: gatewayId,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const response = await callEC2Api<AssociateRouteTableResponse>(
|
|
381
|
+
client,
|
|
382
|
+
"AssociateRouteTable",
|
|
383
|
+
parseRouteTableAssociationXmlResponse,
|
|
384
|
+
params,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
associationId = response.AssociationId;
|
|
388
|
+
state = response.AssociationState?.State || "associated";
|
|
389
|
+
|
|
390
|
+
// Wait for association to be active
|
|
391
|
+
await waitForRouteTableAssociationState(
|
|
392
|
+
client,
|
|
393
|
+
routeTableId,
|
|
394
|
+
associationId,
|
|
395
|
+
"associated",
|
|
396
|
+
timeoutConfig,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Get the current association details to check if it's main
|
|
401
|
+
const describeResponse = await callEC2Api<DescribeRouteTablesResponse>(
|
|
402
|
+
client,
|
|
403
|
+
"DescribeRouteTables",
|
|
404
|
+
parseRouteTableAssociationXmlResponse,
|
|
405
|
+
convertDescribeRouteTablesParamsToAwsFormat({
|
|
406
|
+
RouteTableIds: [routeTableId],
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const routeTable = describeResponse.RouteTables?.[0];
|
|
411
|
+
if (routeTable) {
|
|
412
|
+
const association = routeTable.Associations?.find(
|
|
413
|
+
(a) => a.RouteTableAssociationId === associationId,
|
|
414
|
+
);
|
|
415
|
+
if (association) {
|
|
416
|
+
isMain = association.Main;
|
|
417
|
+
state = association.AssociationState.State;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return this({
|
|
422
|
+
associationId,
|
|
423
|
+
routeTableId,
|
|
424
|
+
subnetId,
|
|
425
|
+
gatewayId,
|
|
426
|
+
isMain,
|
|
427
|
+
state,
|
|
428
|
+
...props,
|
|
429
|
+
routeTable: routeTableId,
|
|
430
|
+
subnet: subnetId,
|
|
431
|
+
gateway: gatewayId,
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Route Table Association timeout constants
|
|
438
|
+
*/
|
|
439
|
+
export const ROUTE_TABLE_ASSOCIATION_TIMEOUT: TimeoutConfig = {
|
|
440
|
+
maxAttempts: 30,
|
|
441
|
+
delayMs: 1000, // 1 second - Route Table Associations are fast resources
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Route Table Association API Parameter Types
|
|
446
|
+
*/
|
|
447
|
+
interface AssociateRouteTableParams {
|
|
448
|
+
RouteTableId: string;
|
|
449
|
+
SubnetId?: string;
|
|
450
|
+
GatewayId?: string;
|
|
451
|
+
DryRun?: boolean;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
interface DisassociateRouteTableParams {
|
|
455
|
+
AssociationId: string;
|
|
456
|
+
DryRun?: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
interface DescribeRouteTablesParams {
|
|
460
|
+
RouteTableIds?: string[];
|
|
461
|
+
Filters?: Filter[];
|
|
462
|
+
DryRun?: boolean;
|
|
463
|
+
NextToken?: string;
|
|
464
|
+
MaxResults?: number;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
interface Filter {
|
|
468
|
+
Name: string;
|
|
469
|
+
Values: string[];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Route Table Association XML Response Types
|
|
474
|
+
*/
|
|
475
|
+
interface AssociateRouteTableXmlResponse {
|
|
476
|
+
associationId: string;
|
|
477
|
+
associationState?: {
|
|
478
|
+
state:
|
|
479
|
+
| "associating"
|
|
480
|
+
| "associated"
|
|
481
|
+
| "disassociating"
|
|
482
|
+
| "disassociated"
|
|
483
|
+
| "failed";
|
|
484
|
+
statusMessage?: string;
|
|
485
|
+
};
|
|
486
|
+
requestId: string;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
interface DisassociateRouteTableXmlResponse {
|
|
490
|
+
return: boolean;
|
|
491
|
+
requestId: string;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
interface ReplaceRouteTableAssociationXmlResponse {
|
|
495
|
+
newAssociationId: string;
|
|
496
|
+
associationState?: {
|
|
497
|
+
state: string;
|
|
498
|
+
statusMessage?: string;
|
|
499
|
+
};
|
|
500
|
+
requestId: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface RouteTableXmlAssociation {
|
|
504
|
+
routeTableAssociationId: string;
|
|
505
|
+
routeTableId: string;
|
|
506
|
+
subnetId?: string;
|
|
507
|
+
gatewayId?: string;
|
|
508
|
+
main: boolean;
|
|
509
|
+
associationState?: {
|
|
510
|
+
state:
|
|
511
|
+
| "associating"
|
|
512
|
+
| "associated"
|
|
513
|
+
| "disassociating"
|
|
514
|
+
| "disassociated"
|
|
515
|
+
| "failed";
|
|
516
|
+
statusMessage?: string;
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
interface RouteTableXmlResult {
|
|
521
|
+
routeTableId: string;
|
|
522
|
+
vpcId: string;
|
|
523
|
+
associationSet?: {
|
|
524
|
+
item: RouteTableXmlAssociation | RouteTableXmlAssociation[];
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
interface DescribeRouteTablesXmlResponse {
|
|
529
|
+
routeTableSet: {
|
|
530
|
+
item: RouteTableXmlResult | RouteTableXmlResult[];
|
|
531
|
+
};
|
|
532
|
+
requestId: string;
|
|
533
|
+
nextToken?: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Helper functions to convert typed parameters to AWS API format
|
|
538
|
+
*/
|
|
539
|
+
function convertAssociateRouteTableParamsToAwsFormat(
|
|
540
|
+
params: AssociateRouteTableParams,
|
|
541
|
+
): Record<string, string> {
|
|
542
|
+
const awsParams: Record<string, string> = {
|
|
543
|
+
RouteTableId: params.RouteTableId,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (params.SubnetId) {
|
|
547
|
+
awsParams.SubnetId = params.SubnetId;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (params.GatewayId) {
|
|
551
|
+
awsParams.GatewayId = params.GatewayId;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (params.DryRun !== undefined) {
|
|
555
|
+
awsParams.DryRun = params.DryRun.toString();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return awsParams;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function convertDisassociateRouteTableParamsToAwsFormat(
|
|
562
|
+
params: DisassociateRouteTableParams,
|
|
563
|
+
): Record<string, string> {
|
|
564
|
+
const awsParams: Record<string, string> = {
|
|
565
|
+
AssociationId: params.AssociationId,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
if (params.DryRun !== undefined) {
|
|
569
|
+
awsParams.DryRun = params.DryRun.toString();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return awsParams;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function convertDescribeRouteTablesParamsToAwsFormat(
|
|
576
|
+
params: DescribeRouteTablesParams,
|
|
577
|
+
): Record<string, string> {
|
|
578
|
+
const awsParams: Record<string, string> = {};
|
|
579
|
+
|
|
580
|
+
if (params.RouteTableIds) {
|
|
581
|
+
params.RouteTableIds.forEach((routeTableId, index) => {
|
|
582
|
+
awsParams[`RouteTableId.${index + 1}`] = routeTableId;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (params.Filters) {
|
|
587
|
+
params.Filters.forEach((filter, filterIndex) => {
|
|
588
|
+
const filterNum = filterIndex + 1;
|
|
589
|
+
awsParams[`Filter.${filterNum}.Name`] = filter.Name;
|
|
590
|
+
|
|
591
|
+
filter.Values.forEach((value, valueIndex) => {
|
|
592
|
+
awsParams[`Filter.${filterNum}.Value.${valueIndex + 1}`] = value;
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (params.DryRun !== undefined) {
|
|
598
|
+
awsParams.DryRun = params.DryRun.toString();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (params.NextToken) {
|
|
602
|
+
awsParams.NextToken = params.NextToken;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (params.MaxResults !== undefined) {
|
|
606
|
+
awsParams.MaxResults = params.MaxResults.toString();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return awsParams;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* A single route table association as returned by the AWS API.
|
|
614
|
+
*/
|
|
615
|
+
interface AwsRouteTableAssociation {
|
|
616
|
+
RouteTableAssociationId: string;
|
|
617
|
+
RouteTableId: string;
|
|
618
|
+
SubnetId?: string;
|
|
619
|
+
GatewayId?: string;
|
|
620
|
+
Main: boolean;
|
|
621
|
+
AssociationState: {
|
|
622
|
+
State: string;
|
|
623
|
+
StatusMessage?: string;
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* AWS Route Table Association API response types
|
|
629
|
+
*/
|
|
630
|
+
interface AssociateRouteTableResponse {
|
|
631
|
+
AssociationId: string;
|
|
632
|
+
AssociationState?: {
|
|
633
|
+
State: string;
|
|
634
|
+
StatusMessage?: string;
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
interface DisassociateRouteTableResponse {
|
|
639
|
+
Return: boolean;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
interface ReplaceRouteTableAssociationResponse {
|
|
643
|
+
NewAssociationId: string;
|
|
644
|
+
AssociationState?: {
|
|
645
|
+
State: string;
|
|
646
|
+
StatusMessage?: string;
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
interface DescribeRouteTablesResponse {
|
|
651
|
+
RouteTables: Array<{
|
|
652
|
+
RouteTableId: string;
|
|
653
|
+
VpcId: string;
|
|
654
|
+
Associations?: AwsRouteTableAssociation[];
|
|
655
|
+
}>;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Parse XML responses specifically for Route Table Association operations
|
|
660
|
+
*/
|
|
661
|
+
function parseRouteTableAssociationXmlResponse<T>(xmlText: string): T {
|
|
662
|
+
const parser = new XMLParser({
|
|
663
|
+
ignoreAttributes: true,
|
|
664
|
+
parseAttributeValue: true,
|
|
665
|
+
parseTagValue: true,
|
|
666
|
+
trimValues: true,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const parsed = parser.parse(xmlText);
|
|
670
|
+
const result: Record<string, unknown> = {};
|
|
671
|
+
|
|
672
|
+
// Parse AssociateRouteTableResponse
|
|
673
|
+
if (parsed.AssociateRouteTableResponse) {
|
|
674
|
+
const response =
|
|
675
|
+
parsed.AssociateRouteTableResponse as AssociateRouteTableXmlResponse;
|
|
676
|
+
result.AssociationId = response.associationId;
|
|
677
|
+
if (response.associationState) {
|
|
678
|
+
result.AssociationState = {
|
|
679
|
+
State: response.associationState.state,
|
|
680
|
+
StatusMessage: response.associationState.statusMessage,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Parse DisassociateRouteTableResponse
|
|
686
|
+
if (parsed.DisassociateRouteTableResponse) {
|
|
687
|
+
const response =
|
|
688
|
+
parsed.DisassociateRouteTableResponse as DisassociateRouteTableXmlResponse;
|
|
689
|
+
result.Return = response.return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Parse ReplaceRouteTableAssociationResponse
|
|
693
|
+
if (parsed.ReplaceRouteTableAssociationResponse) {
|
|
694
|
+
const response =
|
|
695
|
+
parsed.ReplaceRouteTableAssociationResponse as ReplaceRouteTableAssociationXmlResponse;
|
|
696
|
+
result.NewAssociationId = response.newAssociationId;
|
|
697
|
+
if (response.associationState) {
|
|
698
|
+
result.AssociationState = {
|
|
699
|
+
State: response.associationState.state,
|
|
700
|
+
StatusMessage: response.associationState.statusMessage,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Parse DescribeRouteTablesResponse
|
|
706
|
+
if (parsed.DescribeRouteTablesResponse) {
|
|
707
|
+
const response =
|
|
708
|
+
parsed.DescribeRouteTablesResponse as DescribeRouteTablesXmlResponse;
|
|
709
|
+
const routeTableSet = response.routeTableSet;
|
|
710
|
+
if (routeTableSet?.item) {
|
|
711
|
+
const routeTables: RouteTableXmlResult[] = Array.isArray(
|
|
712
|
+
routeTableSet.item,
|
|
713
|
+
)
|
|
714
|
+
? routeTableSet.item
|
|
715
|
+
: [routeTableSet.item];
|
|
716
|
+
result.RouteTables = routeTables.map((routeTable) => {
|
|
717
|
+
const associations = routeTable.associationSet?.item
|
|
718
|
+
? Array.isArray(routeTable.associationSet.item)
|
|
719
|
+
? routeTable.associationSet.item
|
|
720
|
+
: [routeTable.associationSet.item]
|
|
721
|
+
: [];
|
|
722
|
+
return {
|
|
723
|
+
RouteTableId: routeTable.routeTableId,
|
|
724
|
+
VpcId: routeTable.vpcId,
|
|
725
|
+
Associations: associations.map(
|
|
726
|
+
(association): AwsRouteTableAssociation => {
|
|
727
|
+
return {
|
|
728
|
+
RouteTableAssociationId: association.routeTableAssociationId,
|
|
729
|
+
RouteTableId: association.routeTableId,
|
|
730
|
+
SubnetId: association.subnetId,
|
|
731
|
+
GatewayId: association.gatewayId,
|
|
732
|
+
Main: association.main,
|
|
733
|
+
AssociationState: {
|
|
734
|
+
State: association.associationState?.state ?? "associated",
|
|
735
|
+
StatusMessage: association.associationState?.statusMessage,
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
),
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
} else {
|
|
743
|
+
result.RouteTables = [];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return result as T;
|
|
747
|
+
}
|