@reventlessdev/reventless-aws 3.0.0-alpha.218 → 3.0.0-alpha.219
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/CHANGELOG.md +8 -0
- package/package.json +6 -6
- package/src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res +436 -0
- package/src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res.mjs +340 -0
- package/src/adapter/Mcp/MCP_Lambda.res +8 -19
- package/src/adapter/Mcp/MCP_Lambda.res.mjs +2 -9
- package/src/components/Api/AppSync_MergedApi.res +19 -16
- package/src/components/Api/AppSync_MergedApi.res.mjs +5 -6
- package/tests/AppSync_SourceApiAssociation_RetryingTest.res +112 -0
- package/tests/AppSync_SourceApiAssociation_RetryingTest.res.mjs +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.219 (2026-07-21)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** retrying SourceApiAssociation provider for concurrent merged-API deploys ([446a8fe](https://github.com/ReventlessDev/reventless-core/commit/446a8fe77ae9ad94a46eb58ec746f56130974f8e))
|
|
11
|
+
* **core:** populate pluginName + route MCP dispatch through shared helper ([06ec4f9](https://github.com/ReventlessDev/reventless-core/commit/06ec4f98b2dc23a8ef547a754d140e48268ee183))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.218 (2026-07-20)
|
|
7
15
|
|
|
8
16
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.219",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"sury": "11.0.0-alpha.4",
|
|
10
10
|
"uuid": "^13.0.0",
|
|
11
11
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
|
|
12
|
-
"@reventlessdev/rescript-
|
|
13
|
-
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
14
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.173",
|
|
12
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.29",
|
|
15
13
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
16
14
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
15
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.56",
|
|
16
|
+
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
17
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.174",
|
|
17
18
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.102",
|
|
18
19
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.28",
|
|
19
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
20
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.29",
|
|
20
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.38",
|
|
21
21
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.79"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/** AppSync SourceApiAssociation backed by a Pulumi dynamic provider that
|
|
2
|
+
retries CreateSourceApiAssociation (and Delete) on the per-merged-API
|
|
3
|
+
concurrency race:
|
|
4
|
+
|
|
5
|
+
ConcurrentModificationException (HTTP 409)
|
|
6
|
+
|
|
7
|
+
AWS serializes association writes per merged API. When plugin stacks deploy
|
|
8
|
+
in parallel, their first-time associations against the *same* merged API
|
|
9
|
+
race and 409 — the classic `aws:appsync/sourceApiAssociation` provider does
|
|
10
|
+
NOT retry this, so the losing stack's `pulumi up` fails. This provider owns
|
|
11
|
+
the Create / Delete / Get SDK calls and retries the 409 (plus sibling
|
|
12
|
+
transient AWS errors) with capped exponential backoff. It is the Phase-4
|
|
13
|
+
fix that lets plugin deploys run concurrently again without a CI-level
|
|
14
|
+
`max-parallel: 1` serialization (docs/plans/done/merged-api-push-free-composition.md,
|
|
15
|
+
"Association creation 409s under concurrency").
|
|
16
|
+
|
|
17
|
+
Steady-state is unaffected: under AUTO_MERGE, AWS re-merges automatically on
|
|
18
|
+
every source-API schema change with no further association calls, so the
|
|
19
|
+
409 is a creation/deletion-time race only.
|
|
20
|
+
|
|
21
|
+
The initial merge is asynchronous (AUTO_MERGE schedules it); this provider
|
|
22
|
+
returns as soon as the association record exists. Waiting for MERGE_SUCCESS
|
|
23
|
+
stays the caller's job via `AppSync_MergedApi.mergeStatusGateWith`, so a
|
|
24
|
+
failed merge still fails the deploy loudly.
|
|
25
|
+
|
|
26
|
+
State migration: aliases the classic
|
|
27
|
+
`aws:appsync/sourceApiAssociation:SourceApiAssociation` type so existing
|
|
28
|
+
associations are adopted in-place on first deploy (no delete+recreate → no
|
|
29
|
+
merge blip on the shared endpoint).
|
|
30
|
+
|
|
31
|
+
Mirrors `AppSync_Resolver_Retrying`'s dynImport + hand-rolled-retry shape:
|
|
32
|
+
Pulumi serialises the dynamic provider's whole captured closure into stack
|
|
33
|
+
state, and `Effect` (used by the in-process `AppSync_Error` retry) fails
|
|
34
|
+
that serialisation, so the SDK is lazily imported and the backoff loop is
|
|
35
|
+
hand-rolled here. */
|
|
36
|
+
|
|
37
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
38
|
+
|
|
39
|
+
// ── AWS SDK bindings (lazily imported — see AppSync_Resolver_Retrying) ─────────
|
|
40
|
+
|
|
41
|
+
type appSyncClient
|
|
42
|
+
|
|
43
|
+
type mergeConfig = {mergeType: string}
|
|
44
|
+
|
|
45
|
+
type createInput = {
|
|
46
|
+
mergedApiIdentifier: string,
|
|
47
|
+
sourceApiIdentifier: string,
|
|
48
|
+
sourceApiAssociationConfig?: mergeConfig,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type assocSummary = {
|
|
52
|
+
associationId?: string,
|
|
53
|
+
associationArn?: string,
|
|
54
|
+
}
|
|
55
|
+
type createResult = {sourceApiAssociation?: assocSummary}
|
|
56
|
+
type getResult = {sourceApiAssociation?: assocSummary}
|
|
57
|
+
|
|
58
|
+
type idInput = {associationId: string, mergedApiIdentifier: string}
|
|
59
|
+
|
|
60
|
+
type createCmd
|
|
61
|
+
type getCmd
|
|
62
|
+
type deleteCmd
|
|
63
|
+
|
|
64
|
+
// Shape of the dynamically-imported `@aws-sdk/client-appsync` module. Only the
|
|
65
|
+
// three association commands are needed here.
|
|
66
|
+
type sdkModule = {
|
|
67
|
+
@as("AppSyncClient") clientCtor: unit => appSyncClient,
|
|
68
|
+
@as("CreateSourceApiAssociationCommand") createCtor: createInput => createCmd,
|
|
69
|
+
@as("GetSourceApiAssociationCommand") getCtor: idInput => getCmd,
|
|
70
|
+
@as("DeleteSourceApiAssociationCommand") deleteCtor: idInput => deleteCmd,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Calls a constructor with `new` — the SDK exports plain classes.
|
|
74
|
+
let newOf1: ('ctor, 'arg) => 'r = %raw(`(C, x) => new C(x)`)
|
|
75
|
+
let newOf0: 'ctor => 'r = %raw(`(C) => new C()`)
|
|
76
|
+
|
|
77
|
+
@send external sendCreate: (appSyncClient, createCmd) => promise<createResult> = "send"
|
|
78
|
+
@send external sendGet: (appSyncClient, getCmd) => promise<getResult> = "send"
|
|
79
|
+
@send external sendDelete: (appSyncClient, deleteCmd) => promise<unit> = "send"
|
|
80
|
+
|
|
81
|
+
// Dynamic ESM import — emitted as a literal `import("...")` so the bundler /
|
|
82
|
+
// Pulumi serialiser does not statically capture the module in the closure.
|
|
83
|
+
@val external dynImport: string => promise<sdkModule> = "import"
|
|
84
|
+
|
|
85
|
+
let _sdk: ref<option<sdkModule>> = ref(None)
|
|
86
|
+
let _client: ref<option<appSyncClient>> = ref(None)
|
|
87
|
+
|
|
88
|
+
let getSdk = async (): sdkModule =>
|
|
89
|
+
switch _sdk.contents {
|
|
90
|
+
| Some(m) => m
|
|
91
|
+
| None =>
|
|
92
|
+
let m = await dynImport("@aws-sdk/client-appsync")
|
|
93
|
+
_sdk.contents = Some(m)
|
|
94
|
+
m
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let getClient = async (): appSyncClient =>
|
|
98
|
+
switch _client.contents {
|
|
99
|
+
| Some(c) => c
|
|
100
|
+
| None =>
|
|
101
|
+
let sdk = await getSdk()
|
|
102
|
+
let c = newOf0(sdk.clientCtor)
|
|
103
|
+
_client.contents = Some(c)
|
|
104
|
+
c
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Error classification ──────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
@get @return(nullable) external exnName: JsExn.t => option<string> = "name"
|
|
110
|
+
|
|
111
|
+
// Transient AWS errors worth retrying — the same set `AppSync_Error.classify`
|
|
112
|
+
// treats as Transient, matched here by exception name OR message (the SDK
|
|
113
|
+
// surfaces the code in either depending on the error path).
|
|
114
|
+
let transientNames = [
|
|
115
|
+
"ConcurrentModificationException",
|
|
116
|
+
"ThrottlingException",
|
|
117
|
+
"TooManyRequestsException",
|
|
118
|
+
"InternalFailureException",
|
|
119
|
+
"ServiceUnavailableException",
|
|
120
|
+
"ServiceUnavailable",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
/** Returns `true` iff the error is the per-merged-API association concurrency
|
|
124
|
+
race: `ConcurrentModificationException` (HTTP 409). Exposed for testing. */
|
|
125
|
+
let isConcurrentModificationError = (jsErr: JsExn.t): bool =>
|
|
126
|
+
switch (jsErr->exnName, JsExn.message(jsErr)) {
|
|
127
|
+
| (Some("ConcurrentModificationException"), _) => true
|
|
128
|
+
| (_, Some(msg)) => msg->String.includes("ConcurrentModificationException")
|
|
129
|
+
| _ => false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Returns `true` for any transient AWS error the create/delete loop should
|
|
133
|
+
retry (the 409 race plus throttling / transient service errors). */
|
|
134
|
+
let isRetryableAssociationError = (jsErr: JsExn.t): bool =>
|
|
135
|
+
switch (jsErr->exnName, JsExn.message(jsErr)) {
|
|
136
|
+
| (Some(name), _) if transientNames->Array.includes(name) => true
|
|
137
|
+
| (_, Some(msg)) => transientNames->Array.some(n => msg->String.includes(n))
|
|
138
|
+
| _ => false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Returns `true` iff the target association is already gone, so a delete /
|
|
142
|
+
read should be treated as a no-op / drift. AWS reports a missing merged API
|
|
143
|
+
or association as `NotFoundException`. */
|
|
144
|
+
let isAlreadyGoneError = (jsErr: JsExn.t): bool =>
|
|
145
|
+
switch (jsErr->exnName, JsExn.message(jsErr)) {
|
|
146
|
+
| (Some("NotFoundException"), _) => true
|
|
147
|
+
| (_, Some(msg)) => msg->String.includes("NotFoundException") || msg->String.includes("not found")
|
|
148
|
+
| _ => false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Retry helper ─────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
@val external setTimeout: (unit => unit, int) => int = "setTimeout"
|
|
154
|
+
|
|
155
|
+
/** Throws a JavaScript exception directly (bypasses the ReScript exn wrapper),
|
|
156
|
+
so Pulumi displays AWS's real message rather than "error: undefined". */
|
|
157
|
+
let jsThrow: JsExn.t => 'a = %raw(`e => { throw e }`)
|
|
158
|
+
|
|
159
|
+
/** Hand-rolled retry on the transient AppSync association races with capped
|
|
160
|
+
exponential backoff. Delay starts at 2 s, doubles each attempt, caps at
|
|
161
|
+
30 s; up to 8 attempts → ~150 s budget — enough to outlast several serial
|
|
162
|
+
per-merged-API merges (each ~12 s, spike-measured) when a handful of plugin
|
|
163
|
+
stacks associate against one merged API at once. Beyond that, failing loud
|
|
164
|
+
beats retrying forever. */
|
|
165
|
+
let rec runWithRetry = async (
|
|
166
|
+
~attempt: int=0,
|
|
167
|
+
~maxAttempts: int=8,
|
|
168
|
+
~delayMs: int=2000,
|
|
169
|
+
~maxDelayMs: int=30000,
|
|
170
|
+
makeCall: unit => promise<'a>,
|
|
171
|
+
): 'a => {
|
|
172
|
+
try {
|
|
173
|
+
await makeCall()
|
|
174
|
+
} catch {
|
|
175
|
+
| exn =>
|
|
176
|
+
let jsExn = exn->JsExn.fromException
|
|
177
|
+
let name = jsExn->Option.flatMap(exnName)->Option.getOr("(no name)")
|
|
178
|
+
let msg = jsExn->Option.flatMap(JsExn.message)->Option.getOr("(no message)")
|
|
179
|
+
let isRetryable =
|
|
180
|
+
attempt < maxAttempts &&
|
|
181
|
+
jsExn->Option.mapOr(false, isRetryableAssociationError)
|
|
182
|
+
if isRetryable {
|
|
183
|
+
log.info(
|
|
184
|
+
~comp="AppSync_SourceApiAssociation_Retrying",
|
|
185
|
+
`attempt ${(attempt + 1)->Int.toString}/${maxAttempts->Int.toString} failed, retrying in ${delayMs->Int.toString}ms: ${name}: ${msg}`,
|
|
186
|
+
)
|
|
187
|
+
let _ = await Promise.make((resolve, _) => setTimeout(resolve, delayMs)->ignore)
|
|
188
|
+
let nextDelay = delayMs * 2
|
|
189
|
+
let cappedDelay = nextDelay > maxDelayMs ? maxDelayMs : nextDelay
|
|
190
|
+
await runWithRetry(~attempt=attempt + 1, ~maxAttempts, ~delayMs=cappedDelay, ~maxDelayMs, makeCall)
|
|
191
|
+
} else {
|
|
192
|
+
if attempt > 0 {
|
|
193
|
+
log.warn(
|
|
194
|
+
~comp="AppSync_SourceApiAssociation_Retrying",
|
|
195
|
+
`giving up after ${(attempt + 1)->Int.toString} attempts: ${name}: ${msg}`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
switch jsExn {
|
|
199
|
+
| Some(e) => jsThrow(e)
|
|
200
|
+
| None => throw(exn)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Identifier extraction ─────────────────────────────────────────────────────
|
|
207
|
+
//
|
|
208
|
+
// Pulumi passes OUTPUTS (not inputs) to delete/read handlers, and an adopted
|
|
209
|
+
// classic resource carries a different output shape than this provider writes.
|
|
210
|
+
// The resource `id` is always present, so derive (mergedApiId, associationId)
|
|
211
|
+
// from it, falling back to stored props for older/mixed state.
|
|
212
|
+
//
|
|
213
|
+
// - this provider's own id = the association ARN:
|
|
214
|
+
// arn:aws:appsync:<region>:<acct>:apis/<mergedApiId>/sourceApiAssociations/<assocId>
|
|
215
|
+
// - the classic provider's id = "<mergedApiId>,<assocId>"
|
|
216
|
+
|
|
217
|
+
let idsFromArn = (arn: string): option<idInput> => {
|
|
218
|
+
let parts = arn->String.split("/")
|
|
219
|
+
let idx = parts->Array.indexOf("sourceApiAssociations")
|
|
220
|
+
switch (idx >= 1, parts->Array.get(idx - 1), parts->Array.get(idx + 1)) {
|
|
221
|
+
| (true, Some(mergedApiId), Some(assocId)) =>
|
|
222
|
+
Some({mergedApiIdentifier: mergedApiId, associationId: assocId})
|
|
223
|
+
| _ => None
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let idsFromComposite = (id: string): option<idInput> =>
|
|
228
|
+
switch id->String.split(",") {
|
|
229
|
+
| [mergedApiId, assocId] => Some({mergedApiIdentifier: mergedApiId, associationId: assocId})
|
|
230
|
+
| _ => None
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Provider input / output shapes ────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
type providerInputs = {
|
|
236
|
+
mergedApiIdentifier: string,
|
|
237
|
+
sourceApiIdentifier: string,
|
|
238
|
+
mergeType: string,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// `arn` / `associationId` are named to match PulumiAws.AppSync.SourceApiAssociation.t
|
|
242
|
+
// so consumers (mergeStatusGateWith) read `.associationId` unchanged. All fields
|
|
243
|
+
// diff_ compares must live here or `olds.field` is undefined at diff time.
|
|
244
|
+
type createOuts = {
|
|
245
|
+
arn: string,
|
|
246
|
+
associationId: string,
|
|
247
|
+
mergedApiIdentifier: string,
|
|
248
|
+
sourceApiIdentifier: string,
|
|
249
|
+
mergeType: string,
|
|
250
|
+
}
|
|
251
|
+
type createResultOut = {id: string, outs: createOuts}
|
|
252
|
+
type updateResultOut = {outs: createOuts}
|
|
253
|
+
type diffResult = {changes: bool, replaces: array<string>, deleteBeforeReplace: bool}
|
|
254
|
+
type readResult = {id?: string, props?: createOuts}
|
|
255
|
+
|
|
256
|
+
// Extract from stored outs OR classic-shaped props via the id, so delete/read
|
|
257
|
+
// work for both this provider's resources and freshly-adopted classic ones.
|
|
258
|
+
let identifiersFrom = (~id: string, ~props: createOuts): option<idInput> =>
|
|
259
|
+
switch idsFromArn(id) {
|
|
260
|
+
| Some(ids) => Some(ids)
|
|
261
|
+
| None =>
|
|
262
|
+
switch idsFromComposite(id) {
|
|
263
|
+
| Some(ids) => Some(ids)
|
|
264
|
+
| None =>
|
|
265
|
+
// Last resort: this provider's own stored outs.
|
|
266
|
+
switch idsFromArn(props.arn) {
|
|
267
|
+
| Some(ids) => Some(ids)
|
|
268
|
+
| None => None
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Provider methods ──────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
let extractIds = (resp: createResult): (string, string) =>
|
|
276
|
+
switch resp.sourceApiAssociation {
|
|
277
|
+
| Some({associationArn: ?Some(arn), associationId: ?Some(aid)}) => (arn, aid)
|
|
278
|
+
| _ =>
|
|
279
|
+
JsError.throwWithMessage(
|
|
280
|
+
"CreateSourceApiAssociation returned no associationArn / associationId",
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let create = async (inputs: providerInputs): createResultOut => {
|
|
285
|
+
let sdk = await getSdk()
|
|
286
|
+
let client = await getClient()
|
|
287
|
+
let (arn, associationId) = await runWithRetry(() =>
|
|
288
|
+
client
|
|
289
|
+
->sendCreate(
|
|
290
|
+
newOf1(
|
|
291
|
+
sdk.createCtor,
|
|
292
|
+
{
|
|
293
|
+
mergedApiIdentifier: inputs.mergedApiIdentifier,
|
|
294
|
+
sourceApiIdentifier: inputs.sourceApiIdentifier,
|
|
295
|
+
sourceApiAssociationConfig: {mergeType: inputs.mergeType},
|
|
296
|
+
},
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
->Promise.thenResolve(extractIds)
|
|
300
|
+
)
|
|
301
|
+
{
|
|
302
|
+
id: arn,
|
|
303
|
+
outs: {
|
|
304
|
+
arn,
|
|
305
|
+
associationId,
|
|
306
|
+
mergedApiIdentifier: inputs.mergedApiIdentifier,
|
|
307
|
+
sourceApiIdentifier: inputs.sourceApiIdentifier,
|
|
308
|
+
mergeType: inputs.mergeType,
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// No in-place mutation path: identity changes force replace (see diff_), and
|
|
314
|
+
// AUTO_MERGE re-merges are automatic, so update is only ever reached for a
|
|
315
|
+
// no-op reconcile — return the stored outs unchanged.
|
|
316
|
+
let update = async (_id: string, olds: createOuts, _news: providerInputs): updateResultOut => {
|
|
317
|
+
{outs: olds}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let delete_ = async (id: string, props: createOuts): unit => {
|
|
321
|
+
let sdk = await getSdk()
|
|
322
|
+
let client = await getClient()
|
|
323
|
+
switch identifiersFrom(~id, ~props) {
|
|
324
|
+
| None =>
|
|
325
|
+
log.warn(
|
|
326
|
+
~comp="AppSync_SourceApiAssociation_Retrying",
|
|
327
|
+
`delete: could not derive association identifiers from id "${id}"; treating as already gone`,
|
|
328
|
+
)
|
|
329
|
+
| Some(ids) =>
|
|
330
|
+
try {
|
|
331
|
+
await runWithRetry(() => client->sendDelete(newOf1(sdk.deleteCtor, ids)))
|
|
332
|
+
} catch {
|
|
333
|
+
| exn if exn->JsExn.fromException->Option.mapOr(false, isAlreadyGoneError) => ()
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let isDefined: string => bool = %raw(`x => x !== undefined && x !== null`)
|
|
339
|
+
|
|
340
|
+
/** Identity fields force replace — AWS cannot re-point an association's merged
|
|
341
|
+
API or source API in place. Guard undefined olds (adopted classic state)
|
|
342
|
+
so a missing field never spuriously flags a replace. */
|
|
343
|
+
let diff_ = (_id: string, olds: createOuts, news: providerInputs): diffResult => {
|
|
344
|
+
let replaces =
|
|
345
|
+
[
|
|
346
|
+
isDefined(olds.mergedApiIdentifier) && olds.mergedApiIdentifier != news.mergedApiIdentifier
|
|
347
|
+
? Some("mergedApiIdentifier")
|
|
348
|
+
: None,
|
|
349
|
+
isDefined(olds.sourceApiIdentifier) && olds.sourceApiIdentifier != news.sourceApiIdentifier
|
|
350
|
+
? Some("sourceApiIdentifier")
|
|
351
|
+
: None,
|
|
352
|
+
isDefined(olds.mergeType) && olds.mergeType != news.mergeType ? Some("mergeType") : None,
|
|
353
|
+
]->Array.filterMap(x => x)
|
|
354
|
+
// createBeforeDelete: a replace only ever fires on an identity change → a
|
|
355
|
+
// different (mergedApi, sourceApi) pair, so the new association can be made
|
|
356
|
+
// before the old is torn down without a duplicate-pair rejection.
|
|
357
|
+
{changes: replaces->Array.length > 0, replaces, deleteBeforeReplace: false}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Read live state for `pulumi refresh`: GetSourceApiAssociation. Missing
|
|
361
|
+
(association or merged API gone) → return `{}` so Pulumi drops it from state
|
|
362
|
+
and the next `up` recreates it; other errors propagate so refresh fails
|
|
363
|
+
loudly rather than silently discarding the resource. */
|
|
364
|
+
let read_ = async (id: string, props: createOuts): readResult => {
|
|
365
|
+
let sdk = await getSdk()
|
|
366
|
+
let client = await getClient()
|
|
367
|
+
switch identifiersFrom(~id, ~props) {
|
|
368
|
+
| None => ({}: readResult)
|
|
369
|
+
| Some(ids) =>
|
|
370
|
+
try {
|
|
371
|
+
let _ = await client->sendGet(newOf1(sdk.getCtor, ids))
|
|
372
|
+
{id, props}
|
|
373
|
+
} catch {
|
|
374
|
+
| exn if exn->JsExn.fromException->Option.mapOr(false, isAlreadyGoneError) =>
|
|
375
|
+
log.info(
|
|
376
|
+
~comp="AppSync_SourceApiAssociation_Retrying",
|
|
377
|
+
`association ${ids.associationId} on ${ids.mergedApiIdentifier} missing in AppSync; reporting drift`,
|
|
378
|
+
)
|
|
379
|
+
({}: readResult)
|
|
380
|
+
| exn =>
|
|
381
|
+
let jsExn = exn->JsExn.fromException
|
|
382
|
+
switch jsExn {
|
|
383
|
+
| Some(e) => jsThrow(e)
|
|
384
|
+
| None => throw(exn)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Provider as a plain JS object (no Pulumi Output captures — all state flows
|
|
391
|
+
// through inputs / olds / news).
|
|
392
|
+
let provider = {
|
|
393
|
+
"create": create,
|
|
394
|
+
"update": update,
|
|
395
|
+
"delete": delete_,
|
|
396
|
+
"diff": diff_,
|
|
397
|
+
"read": read_,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Pulumi dynamic resource binding ──────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/** Output shape is identical to PulumiAws.AppSync.SourceApiAssociation.t so
|
|
403
|
+
`AppSync_MergedApi.associateSourceWithMergedArn` and `mergeStatusGateWith`
|
|
404
|
+
consume `.id` / `.arn` / `.associationId` unchanged. */
|
|
405
|
+
type t = PulumiAws.AppSync.SourceApiAssociation.t
|
|
406
|
+
|
|
407
|
+
type constructorProps = {
|
|
408
|
+
mergedApiIdentifier: Pulumi.Input.t<string>,
|
|
409
|
+
sourceApiIdentifier: Pulumi.Input.t<string>,
|
|
410
|
+
mergeType: Pulumi.Input.t<string>,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// pulumi.dynamic.Resource constructor: (provider, name, props, opts). Explicit
|
|
414
|
+
// /index.js path because @pulumi/pulumi/dynamic is a directory import not
|
|
415
|
+
// resolvable in ESM mode.
|
|
416
|
+
@module("@pulumi/pulumi/dynamic/index.js") @new
|
|
417
|
+
external _newResource: ('provider, string, 'props, Pulumi.CustomResourceOptions.t) => t = "Resource"
|
|
418
|
+
|
|
419
|
+
let make = (
|
|
420
|
+
~name: string,
|
|
421
|
+
~props: constructorProps,
|
|
422
|
+
~opts: option<Pulumi.CustomResourceOptions.t>,
|
|
423
|
+
): t => {
|
|
424
|
+
// Adopt the classic aws resource previously in state in-place (no
|
|
425
|
+
// delete+recreate → no merge blip on the shared merged endpoint).
|
|
426
|
+
let migrationAlias = Pulumi.Alias.make(
|
|
427
|
+
~type_="aws:appsync/sourceApiAssociation:SourceApiAssociation",
|
|
428
|
+
~name,
|
|
429
|
+
(),
|
|
430
|
+
)
|
|
431
|
+
let finalOpts: Pulumi.CustomResourceOptions.t = switch opts {
|
|
432
|
+
| Some(o) => {...o, aliases: [migrationAlias]}
|
|
433
|
+
| None => {aliases: [migrationAlias]}
|
|
434
|
+
}
|
|
435
|
+
_newResource(provider, name, props, finalOpts)
|
|
436
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Alias$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Alias.res.mjs";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
9
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
10
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
11
|
+
import * as IndexJs from "@pulumi/pulumi/dynamic/index.js";
|
|
12
|
+
|
|
13
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
14
|
+
|
|
15
|
+
let newOf1 = ((C, x) => new C(x));
|
|
16
|
+
|
|
17
|
+
let newOf0 = ((C) => new C());
|
|
18
|
+
|
|
19
|
+
let _sdk = {
|
|
20
|
+
contents: undefined
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let _client = {
|
|
24
|
+
contents: undefined
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function getSdk() {
|
|
28
|
+
let m = _sdk.contents;
|
|
29
|
+
if (m !== undefined) {
|
|
30
|
+
return m;
|
|
31
|
+
}
|
|
32
|
+
let m$1 = await import("@aws-sdk/client-appsync");
|
|
33
|
+
_sdk.contents = m$1;
|
|
34
|
+
return m$1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getClient() {
|
|
38
|
+
let c = _client.contents;
|
|
39
|
+
if (c !== undefined) {
|
|
40
|
+
return Primitive_option.valFromOption(c);
|
|
41
|
+
}
|
|
42
|
+
let sdk = await getSdk();
|
|
43
|
+
let c$1 = newOf0(sdk.AppSyncClient);
|
|
44
|
+
_client.contents = Primitive_option.some(c$1);
|
|
45
|
+
return c$1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let transientNames = [
|
|
49
|
+
"ConcurrentModificationException",
|
|
50
|
+
"ThrottlingException",
|
|
51
|
+
"TooManyRequestsException",
|
|
52
|
+
"InternalFailureException",
|
|
53
|
+
"ServiceUnavailableException",
|
|
54
|
+
"ServiceUnavailable"
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function isConcurrentModificationError(jsErr) {
|
|
58
|
+
let match = jsErr.name;
|
|
59
|
+
let match$1 = Stdlib_JsExn.message(jsErr);
|
|
60
|
+
if (match == null) {
|
|
61
|
+
if (match$1 !== undefined) {
|
|
62
|
+
return match$1.includes("ConcurrentModificationException");
|
|
63
|
+
} else {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
} else if (match === "ConcurrentModificationException") {
|
|
67
|
+
return true;
|
|
68
|
+
} else if (match$1 !== undefined) {
|
|
69
|
+
return match$1.includes("ConcurrentModificationException");
|
|
70
|
+
} else {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isRetryableAssociationError(jsErr) {
|
|
76
|
+
let match = jsErr.name;
|
|
77
|
+
let match$1 = Stdlib_JsExn.message(jsErr);
|
|
78
|
+
if (!(match == null) && transientNames.includes(match)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (match$1 !== undefined) {
|
|
82
|
+
return transientNames.some(n => match$1.includes(n));
|
|
83
|
+
} else {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isAlreadyGoneError(jsErr) {
|
|
89
|
+
let match = jsErr.name;
|
|
90
|
+
let match$1 = Stdlib_JsExn.message(jsErr);
|
|
91
|
+
if (!(match == null) && match === "NotFoundException") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (match$1 !== undefined) {
|
|
95
|
+
if (match$1.includes("NotFoundException")) {
|
|
96
|
+
return true;
|
|
97
|
+
} else {
|
|
98
|
+
return match$1.includes("not found");
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let jsThrow = (e => { throw e });
|
|
106
|
+
|
|
107
|
+
async function runWithRetry(attemptOpt, maxAttemptsOpt, delayMsOpt, maxDelayMsOpt, makeCall) {
|
|
108
|
+
let attempt = attemptOpt !== undefined ? attemptOpt : 0;
|
|
109
|
+
let maxAttempts = maxAttemptsOpt !== undefined ? maxAttemptsOpt : 8;
|
|
110
|
+
let delayMs = delayMsOpt !== undefined ? delayMsOpt : 2000;
|
|
111
|
+
let maxDelayMs = maxDelayMsOpt !== undefined ? maxDelayMsOpt : 30000;
|
|
112
|
+
try {
|
|
113
|
+
return await makeCall();
|
|
114
|
+
} catch (raw_exn) {
|
|
115
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
116
|
+
let jsExn = Stdlib_JsExn.fromException(exn);
|
|
117
|
+
let name = Stdlib_Option.getOr(Stdlib_Option.flatMap(jsExn, prim => Primitive_option.fromNullable(prim.name)), "(no name)");
|
|
118
|
+
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(jsExn, Stdlib_JsExn.message), "(no message)");
|
|
119
|
+
let isRetryable = attempt < maxAttempts && Stdlib_Option.mapOr(jsExn, false, isRetryableAssociationError);
|
|
120
|
+
if (isRetryable) {
|
|
121
|
+
log.info("AppSync_SourceApiAssociation_Retrying", undefined, `attempt ` + (attempt + 1 | 0).toString() + `/` + maxAttempts.toString() + ` failed, retrying in ` + delayMs.toString() + `ms: ` + name + `: ` + msg);
|
|
122
|
+
await new Promise((resolve, param) => {
|
|
123
|
+
setTimeout(resolve, delayMs);
|
|
124
|
+
});
|
|
125
|
+
let nextDelay = (delayMs << 1);
|
|
126
|
+
let cappedDelay = nextDelay > maxDelayMs ? maxDelayMs : nextDelay;
|
|
127
|
+
return await runWithRetry(attempt + 1 | 0, maxAttempts, cappedDelay, maxDelayMs, makeCall);
|
|
128
|
+
}
|
|
129
|
+
if (attempt > 0) {
|
|
130
|
+
log.warn("AppSync_SourceApiAssociation_Retrying", undefined, `giving up after ` + (attempt + 1 | 0).toString() + ` attempts: ` + name + `: ` + msg);
|
|
131
|
+
}
|
|
132
|
+
if (jsExn !== undefined) {
|
|
133
|
+
return jsThrow(Primitive_option.valFromOption(jsExn));
|
|
134
|
+
}
|
|
135
|
+
throw exn;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function idsFromArn(arn) {
|
|
140
|
+
let parts = arn.split("/");
|
|
141
|
+
let idx = parts.indexOf("sourceApiAssociations");
|
|
142
|
+
let match = idx >= 1;
|
|
143
|
+
let match$1 = parts[idx - 1 | 0];
|
|
144
|
+
let match$2 = parts[idx + 1 | 0];
|
|
145
|
+
if (match && match$1 !== undefined && match$2 !== undefined) {
|
|
146
|
+
return {
|
|
147
|
+
associationId: match$2,
|
|
148
|
+
mergedApiIdentifier: match$1
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function idsFromComposite(id) {
|
|
154
|
+
let match = id.split(",");
|
|
155
|
+
if (match.length !== 2) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let mergedApiId = match[0];
|
|
159
|
+
let assocId = match[1];
|
|
160
|
+
return {
|
|
161
|
+
associationId: assocId,
|
|
162
|
+
mergedApiIdentifier: mergedApiId
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function identifiersFrom(id, props) {
|
|
167
|
+
let ids = idsFromArn(id);
|
|
168
|
+
if (ids !== undefined) {
|
|
169
|
+
return ids;
|
|
170
|
+
}
|
|
171
|
+
let ids$1 = idsFromComposite(id);
|
|
172
|
+
if (ids$1 !== undefined) {
|
|
173
|
+
return ids$1;
|
|
174
|
+
}
|
|
175
|
+
let ids$2 = idsFromArn(props.arn);
|
|
176
|
+
if (ids$2 !== undefined) {
|
|
177
|
+
return ids$2;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractIds(resp) {
|
|
182
|
+
let match = resp.sourceApiAssociation;
|
|
183
|
+
if (match === undefined) {
|
|
184
|
+
return Stdlib_JsError.throwWithMessage("CreateSourceApiAssociation returned no associationArn / associationId");
|
|
185
|
+
}
|
|
186
|
+
let aid = match.associationId;
|
|
187
|
+
if (aid === undefined) {
|
|
188
|
+
return Stdlib_JsError.throwWithMessage("CreateSourceApiAssociation returned no associationArn / associationId");
|
|
189
|
+
}
|
|
190
|
+
let arn = match.associationArn;
|
|
191
|
+
if (arn !== undefined) {
|
|
192
|
+
return [
|
|
193
|
+
arn,
|
|
194
|
+
aid
|
|
195
|
+
];
|
|
196
|
+
} else {
|
|
197
|
+
return Stdlib_JsError.throwWithMessage("CreateSourceApiAssociation returned no associationArn / associationId");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function create(inputs) {
|
|
202
|
+
let sdk = await getSdk();
|
|
203
|
+
let client = await getClient();
|
|
204
|
+
let match = await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.CreateSourceApiAssociationCommand, {
|
|
205
|
+
mergedApiIdentifier: inputs.mergedApiIdentifier,
|
|
206
|
+
sourceApiIdentifier: inputs.sourceApiIdentifier,
|
|
207
|
+
sourceApiAssociationConfig: {
|
|
208
|
+
mergeType: inputs.mergeType
|
|
209
|
+
}
|
|
210
|
+
})).then(extractIds));
|
|
211
|
+
let arn = match[0];
|
|
212
|
+
return {
|
|
213
|
+
id: arn,
|
|
214
|
+
outs: {
|
|
215
|
+
arn: arn,
|
|
216
|
+
associationId: match[1],
|
|
217
|
+
mergedApiIdentifier: inputs.mergedApiIdentifier,
|
|
218
|
+
sourceApiIdentifier: inputs.sourceApiIdentifier,
|
|
219
|
+
mergeType: inputs.mergeType
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function update(_id, olds, _news) {
|
|
225
|
+
return {
|
|
226
|
+
outs: olds
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function delete_(id, props) {
|
|
231
|
+
let sdk = await getSdk();
|
|
232
|
+
let client = await getClient();
|
|
233
|
+
let ids = identifiersFrom(id, props);
|
|
234
|
+
if (ids === undefined) {
|
|
235
|
+
return log.warn("AppSync_SourceApiAssociation_Retrying", undefined, `delete: could not derive association identifiers from id "` + id + `"; treating as already gone`);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
return await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.DeleteSourceApiAssociationCommand, ids)));
|
|
239
|
+
} catch (raw_exn) {
|
|
240
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
241
|
+
if (Stdlib_Option.mapOr(Stdlib_JsExn.fromException(exn), false, isAlreadyGoneError)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
throw exn;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let isDefined = (x => x !== undefined && x !== null);
|
|
249
|
+
|
|
250
|
+
function diff_(_id, olds, news) {
|
|
251
|
+
let replaces = Stdlib_Array.filterMap([
|
|
252
|
+
isDefined(olds.mergedApiIdentifier) && olds.mergedApiIdentifier !== news.mergedApiIdentifier ? "mergedApiIdentifier" : undefined,
|
|
253
|
+
isDefined(olds.sourceApiIdentifier) && olds.sourceApiIdentifier !== news.sourceApiIdentifier ? "sourceApiIdentifier" : undefined,
|
|
254
|
+
isDefined(olds.mergeType) && olds.mergeType !== news.mergeType ? "mergeType" : undefined
|
|
255
|
+
], x => x);
|
|
256
|
+
return {
|
|
257
|
+
changes: replaces.length !== 0,
|
|
258
|
+
replaces: replaces,
|
|
259
|
+
deleteBeforeReplace: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function read_(id, props) {
|
|
264
|
+
let sdk = await getSdk();
|
|
265
|
+
let client = await getClient();
|
|
266
|
+
let ids = identifiersFrom(id, props);
|
|
267
|
+
if (ids === undefined) {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await client.send(newOf1(sdk.GetSourceApiAssociationCommand, ids));
|
|
272
|
+
return {
|
|
273
|
+
id: id,
|
|
274
|
+
props: props
|
|
275
|
+
};
|
|
276
|
+
} catch (raw_exn) {
|
|
277
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
278
|
+
if (Stdlib_Option.mapOr(Stdlib_JsExn.fromException(exn), false, isAlreadyGoneError)) {
|
|
279
|
+
log.info("AppSync_SourceApiAssociation_Retrying", undefined, `association ` + ids.associationId + ` on ` + ids.mergedApiIdentifier + ` missing in AppSync; reporting drift`);
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
let jsExn = Stdlib_JsExn.fromException(exn);
|
|
283
|
+
if (jsExn !== undefined) {
|
|
284
|
+
return jsThrow(Primitive_option.valFromOption(jsExn));
|
|
285
|
+
}
|
|
286
|
+
throw exn;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let provider = {
|
|
291
|
+
create: create,
|
|
292
|
+
update: update,
|
|
293
|
+
delete: delete_,
|
|
294
|
+
diff: diff_,
|
|
295
|
+
read: read_
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
function make(name, props, opts) {
|
|
299
|
+
let migrationAlias = Alias$Pulumi.make(name, "aws:appsync/sourceApiAssociation:SourceApiAssociation", undefined);
|
|
300
|
+
let finalOpts;
|
|
301
|
+
if (opts !== undefined) {
|
|
302
|
+
let newrecord = {...opts};
|
|
303
|
+
newrecord.aliases = [migrationAlias];
|
|
304
|
+
finalOpts = newrecord;
|
|
305
|
+
} else {
|
|
306
|
+
finalOpts = {
|
|
307
|
+
aliases: [migrationAlias]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return new IndexJs.Resource(provider, name, props, finalOpts);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export {
|
|
314
|
+
log,
|
|
315
|
+
newOf1,
|
|
316
|
+
newOf0,
|
|
317
|
+
_sdk,
|
|
318
|
+
_client,
|
|
319
|
+
getSdk,
|
|
320
|
+
getClient,
|
|
321
|
+
transientNames,
|
|
322
|
+
isConcurrentModificationError,
|
|
323
|
+
isRetryableAssociationError,
|
|
324
|
+
isAlreadyGoneError,
|
|
325
|
+
jsThrow,
|
|
326
|
+
runWithRetry,
|
|
327
|
+
idsFromArn,
|
|
328
|
+
idsFromComposite,
|
|
329
|
+
identifiersFrom,
|
|
330
|
+
extractIds,
|
|
331
|
+
create,
|
|
332
|
+
update,
|
|
333
|
+
delete_,
|
|
334
|
+
isDefined,
|
|
335
|
+
diff_,
|
|
336
|
+
read_,
|
|
337
|
+
provider,
|
|
338
|
+
make,
|
|
339
|
+
}
|
|
340
|
+
/* log Not a pure module */
|
|
@@ -322,14 +322,6 @@ external makeGenerateCommand: (
|
|
|
322
322
|
external sqsPublishJsons: ('queue, string) => ReventlessCore.CommandGenerator.publishJsons =
|
|
323
323
|
"publishJsons"
|
|
324
324
|
|
|
325
|
-
@module("@reventlessdev/reventless-core/src/RequestContext.res.mjs")
|
|
326
|
-
external requestContextTag: 'a = "tag"
|
|
327
|
-
|
|
328
|
-
@module("effect/Effect")
|
|
329
|
-
external effectProvideService: ('a, 'b) => 'c = "provideService"
|
|
330
|
-
|
|
331
|
-
@send external pipe: ('a, 'b) => 'c = "pipe"
|
|
332
|
-
|
|
333
325
|
let makeQueueRef: string => 'a = %raw(`(url) => ({ id: url, name: url, arn: "" })`)
|
|
334
326
|
|
|
335
327
|
// Decode the payload section of a JWT (base64url → JSON) without signature verification.
|
|
@@ -384,16 +376,6 @@ let extractIdentity = (authHeader: option<string>): Reventless.Identity.t =>
|
|
|
384
376
|
}
|
|
385
377
|
}
|
|
386
378
|
|
|
387
|
-
let runEffect = (correlationId: option<string>, effect) =>
|
|
388
|
-
effect
|
|
389
|
-
->pipe(
|
|
390
|
-
effectProvideService(
|
|
391
|
-
requestContextTag,
|
|
392
|
-
{"correlationId": correlationId->Option.getOr("unknown")},
|
|
393
|
-
),
|
|
394
|
-
)
|
|
395
|
-
->pipe(Effect.runPromise)
|
|
396
|
-
|
|
397
379
|
/** Dispatch an MCP tool call through makeGenerateCommand so the interceptor hook fires.
|
|
398
380
|
The commandTopicArn from mcpConfig tells us which SQS FIFO queue to target. */
|
|
399
381
|
let dispatchTool = async (
|
|
@@ -416,7 +398,14 @@ let dispatchTool = async (
|
|
|
416
398
|
meta: {ip: [], user: identity.userId, info: `mcp/tools/${tool.name}`},
|
|
417
399
|
identity,
|
|
418
400
|
}
|
|
419
|
-
|
|
401
|
+
// Route through the shared dispatch helper so this tool call gets the same
|
|
402
|
+
// correlationId / comp log annotation and a populated RequestContext (identity
|
|
403
|
+
// included) as every other dispatch boundary.
|
|
404
|
+
await ReventlessCore.Runtime.runEffect(
|
|
405
|
+
~comp=`Mcp(${tool.name})`,
|
|
406
|
+
~identity,
|
|
407
|
+
generateCommand(payload),
|
|
408
|
+
)
|
|
420
409
|
}
|
|
421
410
|
|
|
422
411
|
// ─── Deploy-time infrastructure (placeholder) ─────────────────────────────
|
|
@@ -10,13 +10,13 @@ import * as Stream$1 from "effect/Stream";
|
|
|
10
10
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
11
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
12
12
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
13
|
+
import * as Runtime$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Runtime/Runtime.res.mjs";
|
|
13
14
|
import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
|
|
14
15
|
import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
|
|
15
16
|
import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
|
|
16
17
|
import * as MCP_SchemaGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/MCP_SchemaGenerator.res.mjs";
|
|
17
18
|
import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
|
|
18
19
|
import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
19
|
-
import * as RequestContextResMjs from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
|
|
20
20
|
import * as CommandTopicChannel_SQS_RuntimeResMjs from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
21
21
|
import * as CommandGenerator_CallbackResMjs from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
22
22
|
|
|
@@ -247,12 +247,6 @@ function extractIdentity(authHeader) {
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
function runEffect(correlationId, effect) {
|
|
251
|
-
return effect.pipe(Effect.provideService(RequestContextResMjs.tag, {
|
|
252
|
-
correlationId: Stdlib_Option.getOr(correlationId, "unknown")
|
|
253
|
-
})).pipe(prim => Effect.runPromise(prim));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
250
|
async function dispatchTool(tool, args, identity) {
|
|
257
251
|
let queueRef = makeQueueRef(tool.commandTopicArn);
|
|
258
252
|
let publishJsons = CommandTopicChannel_SQS_RuntimeResMjs.publishJsons(queueRef, AWS$ReventlessAws.SQS_FIFO.service);
|
|
@@ -269,7 +263,7 @@ async function dispatchTool(tool, args, identity) {
|
|
|
269
263
|
meta: payload_meta,
|
|
270
264
|
identity: identity
|
|
271
265
|
};
|
|
272
|
-
return await runEffect(undefined, generateCommand(payload));
|
|
266
|
+
return await Runtime$ReventlessCore.runEffect(undefined, undefined, `Mcp(` + tool.name + `)`, undefined, identity, undefined, generateCommand(payload));
|
|
273
267
|
}
|
|
274
268
|
|
|
275
269
|
export {
|
|
@@ -283,7 +277,6 @@ export {
|
|
|
283
277
|
makeQueueRef,
|
|
284
278
|
decodeJwtClaims,
|
|
285
279
|
extractIdentity,
|
|
286
|
-
runEffect,
|
|
287
280
|
dispatchTool,
|
|
288
281
|
}
|
|
289
282
|
/* S Not a pure module */
|
|
@@ -122,9 +122,11 @@ let make = (~name: string, ~opts: Pulumi.ComponentResource.options): t => {
|
|
|
122
122
|
|
|
123
123
|
/** Associate a source API with the merged API under AUTO_MERGE. AWS
|
|
124
124
|
serializes association CREATES per merged API (409
|
|
125
|
-
ConcurrentModificationException)
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
ConcurrentModificationException). This platform-stack form stays on the
|
|
126
|
+
classic provider: the platform stack creates all its own associations
|
|
127
|
+
(admin + base) sequentially within one program, so they never race. The
|
|
128
|
+
cross-stack race is on the plugin-stack `associateSourceWithMergedArn`
|
|
129
|
+
path, which uses the retrying provider (Phase 4). */
|
|
128
130
|
let associateSource = (
|
|
129
131
|
~name: string,
|
|
130
132
|
~mergedApi: t,
|
|
@@ -158,8 +160,15 @@ let associateSource = (
|
|
|
158
160
|
/** Associate a source API against a merged API referenced by ARN — the
|
|
159
161
|
plugin-stack form, where the merged API is not a resource in this stack
|
|
160
162
|
but the platform's `domainMergedApiArn` / `platformMergedApiArn`
|
|
161
|
-
StackReference export.
|
|
162
|
-
|
|
163
|
+
StackReference export.
|
|
164
|
+
|
|
165
|
+
Concurrent first-time plugin deploys race here: AWS serializes association
|
|
166
|
+
writes per merged API and 409s (`ConcurrentModificationException`). This
|
|
167
|
+
goes through `AppSync_SourceApiAssociation_Retrying`, a dynamic provider
|
|
168
|
+
that retries the 409 with backoff (Phase 4), so plugin stacks can deploy in
|
|
169
|
+
parallel without a CI-level serialization. The returned resource keeps the
|
|
170
|
+
`PulumiAws.AppSync.SourceApiAssociation.t` shape, so `mergeStatusGateWith`
|
|
171
|
+
still waits for MERGE_SUCCESS on `.associationId` unchanged. */
|
|
163
172
|
let associateSourceWithMergedArn = (
|
|
164
173
|
~name: string,
|
|
165
174
|
~mergedApiArn: Pulumi.Output.t<string>,
|
|
@@ -169,20 +178,14 @@ let associateSourceWithMergedArn = (
|
|
|
169
178
|
let customOpts: Pulumi.CustomResourceOptions.t = {
|
|
170
179
|
parent: ?opts.parent,
|
|
171
180
|
}
|
|
172
|
-
|
|
181
|
+
AppSync_SourceApiAssociation_Retrying.make(
|
|
173
182
|
~name,
|
|
174
|
-
~
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
~props={
|
|
184
|
+
mergedApiIdentifier: mergedApiArn->Pulumi.Output.asInput,
|
|
185
|
+
sourceApiIdentifier: sourceApi
|
|
177
186
|
->Pulumi.Output.flatMap((a: AppSync.GraphQLApi.t) => a.id)
|
|
178
187
|
->Pulumi.Output.asInput,
|
|
179
|
-
|
|
180
|
-
(
|
|
181
|
-
{
|
|
182
|
-
mergeType: AppSync.SourceApiAssociation.AUTO_MERGE->Pulumi.Input.make,
|
|
183
|
-
}: AppSync.SourceApiAssociation.sourceApiAssociationConfig
|
|
184
|
-
)->Pulumi.Input.make,
|
|
185
|
-
]->Pulumi.Input.make,
|
|
188
|
+
mergeType: "AUTO_MERGE"->Pulumi.Input.make,
|
|
186
189
|
},
|
|
187
190
|
~opts=Some(customOpts),
|
|
188
191
|
)
|
|
@@ -7,6 +7,7 @@ import * as Pulumi from "@pulumi/pulumi";
|
|
|
7
7
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
8
8
|
import * as Auth_Cognito$ReventlessAws from "../../adapter/Auth/Auth_Cognito.res.mjs";
|
|
9
9
|
import * as AppSync_Adapter$ReventlessAws from "./AppSync_Adapter.res.mjs";
|
|
10
|
+
import * as AppSync_SourceApiAssociation_Retrying$ReventlessAws from "../../adapter/Api/AppSync_SourceApiAssociation_Retrying.res.mjs";
|
|
10
11
|
|
|
11
12
|
function authenticationTypeName(t) {
|
|
12
13
|
switch (t) {
|
|
@@ -89,12 +90,10 @@ function associateSourceWithMergedArn(name, mergedApiArn, sourceApi, opts) {
|
|
|
89
90
|
let customOpts = {
|
|
90
91
|
parent: customOpts_parent
|
|
91
92
|
};
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
mergeType: "AUTO_MERGE"
|
|
97
|
-
}]
|
|
93
|
+
return AppSync_SourceApiAssociation_Retrying$ReventlessAws.make(name, {
|
|
94
|
+
mergedApiIdentifier: mergedApiArn,
|
|
95
|
+
sourceApiIdentifier: Output$Pulumi.flatMap(sourceApi, a => a.id),
|
|
96
|
+
mergeType: "AUTO_MERGE"
|
|
98
97
|
}, customOpts);
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Build a plain JS object shaped like a JS Error / SDK exception, cast to
|
|
4
|
+
// JsExn.t so the classifiers can be called without a real throw/catch.
|
|
5
|
+
type testError = {name: string, message: string}
|
|
6
|
+
let mkErr = (~name, ~message): JsExn.t => Obj.magic(({name, message}: testError))
|
|
7
|
+
|
|
8
|
+
module Assoc = AppSync_SourceApiAssociation_Retrying
|
|
9
|
+
|
|
10
|
+
describe("AppSync_SourceApiAssociation_Retrying.isConcurrentModificationError", () => {
|
|
11
|
+
testSync("true for ConcurrentModificationException by name", () => {
|
|
12
|
+
let err = mkErr(~name="ConcurrentModificationException", ~message="Schema is currently being merged")
|
|
13
|
+
expect(Assoc.isConcurrentModificationError(err))->toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
testSync("true when the name rides in the message", () => {
|
|
17
|
+
let err = mkErr(~name="Error", ~message="ConcurrentModificationException: try again")
|
|
18
|
+
expect(Assoc.isConcurrentModificationError(err))->toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
testSync("false for an unrelated error", () => {
|
|
22
|
+
let err = mkErr(~name="ValidationException", ~message="bad SDL")
|
|
23
|
+
expect(Assoc.isConcurrentModificationError(err))->toBe(false)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("AppSync_SourceApiAssociation_Retrying.isRetryableAssociationError", () => {
|
|
28
|
+
describe("returns true", () => {
|
|
29
|
+
[
|
|
30
|
+
"ConcurrentModificationException",
|
|
31
|
+
"ThrottlingException",
|
|
32
|
+
"TooManyRequestsException",
|
|
33
|
+
"InternalFailureException",
|
|
34
|
+
"ServiceUnavailableException",
|
|
35
|
+
]->Array.forEach(name =>
|
|
36
|
+
testSync(name, () => {
|
|
37
|
+
expect(Assoc.isRetryableAssociationError(mkErr(~name, ~message="x")))->toBe(true)
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe("returns false", () => {
|
|
43
|
+
testSync("permanent ValidationException", () => {
|
|
44
|
+
expect(
|
|
45
|
+
Assoc.isRetryableAssociationError(mkErr(~name="ValidationException", ~message="bad input")),
|
|
46
|
+
)->toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
testSync("NotFoundException (a delete no-op, not a retry)", () => {
|
|
50
|
+
expect(
|
|
51
|
+
Assoc.isRetryableAssociationError(
|
|
52
|
+
mkErr(~name="NotFoundException", ~message="association not found"),
|
|
53
|
+
),
|
|
54
|
+
)->toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
testSync("non-exception value", () => {
|
|
58
|
+
let notErr: JsExn.t = Obj.magic(42)
|
|
59
|
+
expect(Assoc.isRetryableAssociationError(notErr))->toBe(false)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe("AppSync_SourceApiAssociation_Retrying.isAlreadyGoneError", () => {
|
|
65
|
+
testSync("true for NotFoundException", () => {
|
|
66
|
+
expect(Assoc.isAlreadyGoneError(mkErr(~name="NotFoundException", ~message="gone")))->toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
testSync("true when message says not found", () => {
|
|
70
|
+
expect(Assoc.isAlreadyGoneError(mkErr(~name="Error", ~message="merged API not found")))->toBe(
|
|
71
|
+
true,
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
testSync("false for a transient race (must not be swallowed as gone)", () => {
|
|
76
|
+
expect(
|
|
77
|
+
Assoc.isAlreadyGoneError(mkErr(~name="ConcurrentModificationException", ~message="retry")),
|
|
78
|
+
)->toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe("AppSync_SourceApiAssociation_Retrying.idsFromArn", () => {
|
|
83
|
+
testSync("parses mergedApiId + associationId from an association ARN", () => {
|
|
84
|
+
let arn = "arn:aws:appsync:eu-west-1:123456789012:apis/mgd123/sourceApiAssociations/assoc456"
|
|
85
|
+
switch Assoc.idsFromArn(arn) {
|
|
86
|
+
| Some(ids) =>
|
|
87
|
+
expect(ids.mergedApiIdentifier)->toBe("mgd123")
|
|
88
|
+
expect(ids.associationId)->toBe("assoc456")
|
|
89
|
+
| None => fail("expected Some")
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
testSync("None for an ARN that is not an association", () => {
|
|
94
|
+
let arn = "arn:aws:appsync:eu-west-1:123456789012:apis/mgd123"
|
|
95
|
+
expect(Assoc.idsFromArn(arn)->Option.isNone)->toBe(true)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("AppSync_SourceApiAssociation_Retrying.idsFromComposite", () => {
|
|
100
|
+
testSync("parses the classic '<mergedApiId>,<assocId>' id", () => {
|
|
101
|
+
switch Assoc.idsFromComposite("mgd123,assoc456") {
|
|
102
|
+
| Some(ids) =>
|
|
103
|
+
expect(ids.mergedApiIdentifier)->toBe("mgd123")
|
|
104
|
+
expect(ids.associationId)->toBe("assoc456")
|
|
105
|
+
| None => fail("expected Some")
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
testSync("None for a non-composite string", () => {
|
|
110
|
+
expect(Assoc.idsFromComposite("just-one-value")->Option.isNone)->toBe(true)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as AppSync_SourceApiAssociation_Retrying$ReventlessAws from "../src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res.mjs";
|
|
6
|
+
|
|
7
|
+
function mkErr(name, message) {
|
|
8
|
+
return {
|
|
9
|
+
name: name,
|
|
10
|
+
message: message
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
globalThis.describe("AppSync_SourceApiAssociation_Retrying.isConcurrentModificationError", () => {
|
|
15
|
+
globalThis.test("true for ConcurrentModificationException by name", () => {
|
|
16
|
+
let err = {
|
|
17
|
+
name: "ConcurrentModificationException",
|
|
18
|
+
message: "Schema is currently being merged"
|
|
19
|
+
};
|
|
20
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isConcurrentModificationError(err)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
globalThis.test("true when the name rides in the message", () => {
|
|
23
|
+
let err = {
|
|
24
|
+
name: "Error",
|
|
25
|
+
message: "ConcurrentModificationException: try again"
|
|
26
|
+
};
|
|
27
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isConcurrentModificationError(err)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
globalThis.test("false for an unrelated error", () => {
|
|
30
|
+
let err = {
|
|
31
|
+
name: "ValidationException",
|
|
32
|
+
message: "bad SDL"
|
|
33
|
+
};
|
|
34
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isConcurrentModificationError(err)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
globalThis.describe("AppSync_SourceApiAssociation_Retrying.isRetryableAssociationError", () => {
|
|
39
|
+
globalThis.describe("returns true", () => {
|
|
40
|
+
[
|
|
41
|
+
"ConcurrentModificationException",
|
|
42
|
+
"ThrottlingException",
|
|
43
|
+
"TooManyRequestsException",
|
|
44
|
+
"InternalFailureException",
|
|
45
|
+
"ServiceUnavailableException"
|
|
46
|
+
].forEach(name => {
|
|
47
|
+
globalThis.test(name, () => {
|
|
48
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isRetryableAssociationError({
|
|
49
|
+
name: name,
|
|
50
|
+
message: "x"
|
|
51
|
+
})).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
globalThis.describe("returns false", () => {
|
|
56
|
+
globalThis.test("permanent ValidationException", () => {
|
|
57
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isRetryableAssociationError({
|
|
58
|
+
name: "ValidationException",
|
|
59
|
+
message: "bad input"
|
|
60
|
+
})).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
globalThis.test("NotFoundException (a delete no-op, not a retry)", () => {
|
|
63
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isRetryableAssociationError({
|
|
64
|
+
name: "NotFoundException",
|
|
65
|
+
message: "association not found"
|
|
66
|
+
})).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
globalThis.test("non-exception value", () => {
|
|
69
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isRetryableAssociationError(42)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
globalThis.describe("AppSync_SourceApiAssociation_Retrying.isAlreadyGoneError", () => {
|
|
75
|
+
globalThis.test("true for NotFoundException", () => {
|
|
76
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isAlreadyGoneError({
|
|
77
|
+
name: "NotFoundException",
|
|
78
|
+
message: "gone"
|
|
79
|
+
})).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
globalThis.test("true when message says not found", () => {
|
|
82
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isAlreadyGoneError({
|
|
83
|
+
name: "Error",
|
|
84
|
+
message: "merged API not found"
|
|
85
|
+
})).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
globalThis.test("false for a transient race (must not be swallowed as gone)", () => {
|
|
88
|
+
globalThis.expect(AppSync_SourceApiAssociation_Retrying$ReventlessAws.isAlreadyGoneError({
|
|
89
|
+
name: "ConcurrentModificationException",
|
|
90
|
+
message: "retry"
|
|
91
|
+
})).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
globalThis.describe("AppSync_SourceApiAssociation_Retrying.idsFromArn", () => {
|
|
96
|
+
globalThis.test("parses mergedApiId + associationId from an association ARN", () => {
|
|
97
|
+
let ids = AppSync_SourceApiAssociation_Retrying$ReventlessAws.idsFromArn("arn:aws:appsync:eu-west-1:123456789012:apis/mgd123/sourceApiAssociations/assoc456");
|
|
98
|
+
if (ids !== undefined) {
|
|
99
|
+
globalThis.expect(ids.mergedApiIdentifier).toBe("mgd123");
|
|
100
|
+
globalThis.expect(ids.associationId).toBe("assoc456");
|
|
101
|
+
return;
|
|
102
|
+
} else {
|
|
103
|
+
return JestGlobals.fail("expected Some");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
globalThis.test("None for an ARN that is not an association", () => {
|
|
107
|
+
globalThis.expect(Stdlib_Option.isNone(AppSync_SourceApiAssociation_Retrying$ReventlessAws.idsFromArn("arn:aws:appsync:eu-west-1:123456789012:apis/mgd123"))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
globalThis.describe("AppSync_SourceApiAssociation_Retrying.idsFromComposite", () => {
|
|
112
|
+
globalThis.test("parses the classic '<mergedApiId>,<assocId>' id", () => {
|
|
113
|
+
let ids = AppSync_SourceApiAssociation_Retrying$ReventlessAws.idsFromComposite("mgd123,assoc456");
|
|
114
|
+
if (ids !== undefined) {
|
|
115
|
+
globalThis.expect(ids.mergedApiIdentifier).toBe("mgd123");
|
|
116
|
+
globalThis.expect(ids.associationId).toBe("assoc456");
|
|
117
|
+
return;
|
|
118
|
+
} else {
|
|
119
|
+
return JestGlobals.fail("expected Some");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("None for a non-composite string", () => {
|
|
123
|
+
globalThis.expect(Stdlib_Option.isNone(AppSync_SourceApiAssociation_Retrying$ReventlessAws.idsFromComposite("just-one-value"))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
let Assoc;
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
mkErr,
|
|
131
|
+
Assoc,
|
|
132
|
+
}
|
|
133
|
+
/* Not a pure module */
|