@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.4 → 3.0.0-alpha.40
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 +300 -0
- package/package.json +8 -7
- package/rescript.json +2 -1
- package/src/AppSync/AppSync_ResolverRuntime.res +4 -5
- package/src/AppSync/AppSync_Resolver_Functions.res +281 -169
- package/src/AppSync/AppSync_Resolver_Functions.res.mjs +116 -72
- package/src/AppSync/AppSync_SourceApiAssociation.res +1 -3
- package/src/AwsNative/AppSync/AwsNative_AppSync_Api.res +2 -3
- package/src/AwsNative/AppSync/AwsNative_AppSync_ChannelNamespace.res +2 -6
- package/src/AwsNative/AppSync/AwsNative_AppSync_Resolver.res +2 -5
- package/src/Cloudwatch/Cloudwatch_EventRule.res +4 -0
- package/src/Cloudwatch/Cloudwatch_EventTarget.res +11 -0
- package/src/Cloudwatch/Cloudwatch_LogMetricFilter.res +0 -1
- package/src/Cloudwatch/Cloudwatch_MetricAlarm.res +1 -2
- package/src/Cognito/Cognito_UserPool.res +2 -4
- package/src/IAM/IAM.res +1 -1
- package/src/IAM/PolicyDocument.res +2 -2
- package/src/IAM/PolicyDocument.res.mjs +3 -3
- package/src/Kinesis/Kinesis_Stream.res +3 -6
- package/src/Kms/Kms.res +5 -0
- package/src/Kms/Kms.res.mjs +12 -0
- package/src/Kms/Kms_Alias.res +16 -0
- package/src/Kms/Kms_Alias.res.mjs +2 -0
- package/src/Kms/Kms_Key.res +23 -0
- package/src/Kms/Kms_Key.res.mjs +2 -0
- package/src/Lambda/FunctionUrl.res +5 -2
- package/src/Lambda/Lambda.res +139 -53
- package/src/Lambda/Lambda.res.mjs +152 -23
- package/src/Rds/Rds_Cluster.res +0 -1
- package/src/Rds/Rds_Instance.res +0 -1
- package/src/Rds/Rds_Proxy.res +1 -2
- package/src/S3/S3_Bucket.res +1 -1
- package/src/S3/S3_BucketV2.res +1 -3
- package/src/SNS/SNS_Topic.res +9 -9
- package/src/Scheduler/Scheduler.res +4 -0
- package/src/Scheduler/Scheduler.res.mjs +9 -0
- package/src/Scheduler/Scheduler_ScheduleGroup.res +14 -0
- package/src/Scheduler/Scheduler_ScheduleGroup.res.mjs +2 -0
- package/src/SecretsManager/GetSecretVersion.res +1 -1
- package/src/example/AlarmMailExample.res +34 -0
- package/src/example/AlarmMailExample.res.mjs +44 -0
- package/src/example/CustomDomainExample.res +2 -3
- package/src/example/MergedApiExample.res +1 -3
- package/tests/AppSync_Resolver_FunctionsTest.mjs +141 -23
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
let mergedExecRole = IAM.Role.make(
|
|
9
9
|
~name="example-merged-exec-role",
|
|
10
10
|
~args={
|
|
11
|
-
assumeRolePolicy: Pulumi.Input.make(
|
|
12
|
-
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`,
|
|
13
|
-
),
|
|
11
|
+
assumeRolePolicy: Pulumi.Input.make(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`),
|
|
14
12
|
},
|
|
15
13
|
)
|
|
16
14
|
|
|
@@ -158,7 +158,7 @@ describe('listAllItemsConnection', () => {
|
|
|
158
158
|
const ctx = makeCtx({ args: {} })
|
|
159
159
|
const result = request(ctx)
|
|
160
160
|
expect(result.operation).toBe('Scan')
|
|
161
|
-
expect(result.limit).toBe(
|
|
161
|
+
expect(result.limit).toBe(51)
|
|
162
162
|
expect(result.nextToken).toBeNull()
|
|
163
163
|
expect(result.filter).toBeUndefined()
|
|
164
164
|
})
|
|
@@ -209,6 +209,11 @@ describe('listAllItemsConnection', () => {
|
|
|
209
209
|
expect(req2.nextToken).toBe('TOK1')
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
+
// `hasPreviousPage` reports what this door can SERVE, not what exists. This
|
|
213
|
+
// cursor opens a new window at position 0, so the page before it lies in the
|
|
214
|
+
// previous window — real, and unreachable, because a continuation token
|
|
215
|
+
// cannot name the one before it. Claiming it is what drew a Prev button that
|
|
216
|
+
// always errored.
|
|
212
217
|
test('final page (nextToken null) closes the connection', () => {
|
|
213
218
|
const after = util.base64Encode(JSON.stringify({ t: 'TOK1', n: -1 }))
|
|
214
219
|
const r = response(makeCtx({
|
|
@@ -216,7 +221,7 @@ describe('listAllItemsConnection', () => {
|
|
|
216
221
|
result: { items: [{ id: 'z' }], nextToken: null },
|
|
217
222
|
}))
|
|
218
223
|
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
219
|
-
expect(r.pageInfo.hasPreviousPage).toBe(
|
|
224
|
+
expect(r.pageInfo.hasPreviousPage).toBe(false)
|
|
220
225
|
})
|
|
221
226
|
|
|
222
227
|
// A filtered Scan can return an empty page while nextToken is still set. The
|
|
@@ -239,15 +244,55 @@ describe('listAllItemsConnection', () => {
|
|
|
239
244
|
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
240
245
|
})
|
|
241
246
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
// `before` is served by re-reading the window the cursor names and cutting
|
|
248
|
+
// the page that ends at it. `last` is not: the last N rows of the list needs
|
|
249
|
+
// the END of the list, which a forward-only cursor cannot reach.
|
|
250
|
+
test('request rejects `last`, which needs the end of the list', () => {
|
|
251
|
+
expect(() => request(makeCtx({ args: { last: 5 } }))).toThrow('last is not supported')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('a backward page is the page that precedes it, exactly', () => {
|
|
255
|
+
const items = Array.from({ length: 25 }, (_, i) => ({ id: 'r' + i }))
|
|
256
|
+
const page1 = response(makeCtx({ args: { first: 10 }, result: { items, nextToken: null } }))
|
|
257
|
+
const fwd = { args: { first: 10, after: page1.pageInfo.endCursor } }
|
|
258
|
+
const page2 = response(makeCtx({ ...fwd, result: { items, nextToken: null } }))
|
|
259
|
+
expect(page2.edges.map(e => e.node.id)).toEqual(items.slice(10, 20).map(i => i.id))
|
|
260
|
+
|
|
261
|
+
const back = { args: { first: 10, before: page2.pageInfo.startCursor } }
|
|
262
|
+
const prev = response(makeCtx({ ...back, result: { items, nextToken: null } }))
|
|
263
|
+
expect(prev.edges.map(e => e.node.id)).toEqual(page1.edges.map(e => e.node.id))
|
|
264
|
+
// Cut from ahead, so a next page provably exists — the one we came from.
|
|
265
|
+
expect(prev.pageInfo.hasNextPage).toBe(true)
|
|
266
|
+
expect(prev.pageInfo.hasPreviousPage).toBe(false)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// The read must reach `_upTo` rows into the window, not `first + from`, or the
|
|
270
|
+
// backward slice comes back with its tail missing.
|
|
271
|
+
test('a backward read examines the window as deep as the page it cuts', () => {
|
|
272
|
+
const before = util.base64Encode(JSON.stringify({ t: null, n: 30 }))
|
|
273
|
+
expect(request(makeCtx({ args: { first: 10, before } })).limit).toBe(30)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('the window a backward cursor names is the one re-read', () => {
|
|
277
|
+
const before = util.base64Encode(JSON.stringify({ t: 'W7', n: 30 }))
|
|
278
|
+
expect(request(makeCtx({ args: { first: 10, before } })).nextToken).toBe('W7')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// The one page still out of reach, and the only case left refused.
|
|
282
|
+
test('a previous page in an earlier window is refused, and says which limit', () => {
|
|
283
|
+
const before = util.base64Encode(JSON.stringify({ t: 'W2', n: 0 }))
|
|
284
|
+
expect(() => request(makeCtx({ args: { first: 10, before } }))).toThrow(
|
|
285
|
+
'earlier read window',
|
|
246
286
|
)
|
|
247
287
|
})
|
|
248
288
|
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
// At the very start there is no earlier window to be unable to name, so a
|
|
290
|
+
// short backward page is served rather than refused.
|
|
291
|
+
test('a backward page at the start of the first window is served, not refused', () => {
|
|
292
|
+
const items = Array.from({ length: 10 }, (_, i) => ({ id: 'r' + i }))
|
|
293
|
+
const before = util.base64Encode(JSON.stringify({ t: null, n: 3 }))
|
|
294
|
+
const r = response(makeCtx({ args: { first: 10, before }, result: { items, nextToken: null } }))
|
|
295
|
+
expect(r.edges.map(e => e.node.id)).toEqual(['r0', 'r1', 'r2'])
|
|
251
296
|
})
|
|
252
297
|
|
|
253
298
|
// Cursors minted before the read window existed named the following window.
|
|
@@ -272,10 +317,11 @@ describe('listAllItemsConnection', () => {
|
|
|
272
317
|
F.listAllItemsConnection('name', [], [], [], undefined, 'customerId', ['Admin']),
|
|
273
318
|
)
|
|
274
319
|
|
|
275
|
-
|
|
320
|
+
// One row past the page: it is what says a next page exists.
|
|
321
|
+
test('an unfiltered read examines one row past the page it serves', () => {
|
|
276
322
|
const req = request(makeCtx({ args: { first: 25 }, identity: null }))
|
|
277
323
|
expect(req.filter).toBeUndefined()
|
|
278
|
-
expect(req.limit).toBe(
|
|
324
|
+
expect(req.limit).toBe(26)
|
|
279
325
|
})
|
|
280
326
|
|
|
281
327
|
test('a filtered read examines a wider window than the page', () => {
|
|
@@ -285,7 +331,7 @@ describe('listAllItemsConnection', () => {
|
|
|
285
331
|
})
|
|
286
332
|
|
|
287
333
|
test('a caller asking for more than the window gets what it asked for', () => {
|
|
288
|
-
expect(request(makeCtx({ args: { first: 5000 } })).limit).toBe(
|
|
334
|
+
expect(request(makeCtx({ args: { first: 5000 } })).limit).toBe(5001)
|
|
289
335
|
})
|
|
290
336
|
|
|
291
337
|
// The user-visible bug: one matching row must be one page, not three.
|
|
@@ -328,6 +374,34 @@ describe('listAllItemsConnection', () => {
|
|
|
328
374
|
).toBe('W2')
|
|
329
375
|
})
|
|
330
376
|
|
|
377
|
+
// DynamoDB hands back a token whenever a read stops AT its limit, whether or
|
|
378
|
+
// not anything follows. A read of exactly the page therefore claimed a next
|
|
379
|
+
// page after the last full one, and it came back blank — found on a deployed
|
|
380
|
+
// list paged one row at a time. The mock pages a table the way DynamoDB does.
|
|
381
|
+
test('paging to the end serves no blank page, whatever the page size', () => {
|
|
382
|
+
const rows = ['a', 'b', 'c', 'd'].map(id => ({ id }))
|
|
383
|
+
const dynamo = req => {
|
|
384
|
+
const start = req.nextToken === null ? 0 : +req.nextToken
|
|
385
|
+
const items = rows.slice(start, start + req.limit)
|
|
386
|
+
const stoppedAtLimit = items.length === req.limit
|
|
387
|
+
return { items, nextToken: stoppedAtLimit ? String(start + req.limit) : null }
|
|
388
|
+
}
|
|
389
|
+
;[1, 2, 4].map(first => {
|
|
390
|
+
const pages = []
|
|
391
|
+
let after
|
|
392
|
+
for (let i = 0; i < 10; i++) {
|
|
393
|
+
const args = after === undefined ? { first } : { first, after }
|
|
394
|
+
const r = response(makeCtx({ args, result: dynamo(request(makeCtx({ args, identity: null }))) }))
|
|
395
|
+
pages.push(r.edges.map(e => e.node.id))
|
|
396
|
+
if (!r.pageInfo.hasNextPage) break
|
|
397
|
+
after = r.pageInfo.endCursor
|
|
398
|
+
}
|
|
399
|
+
expect(pages.flat()).toEqual(['a', 'b', 'c', 'd'])
|
|
400
|
+
expect(pages.every(p => p.length > 0)).toBe(true)
|
|
401
|
+
expect(pages).toHaveLength(Math.ceil(4 / first))
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
331
405
|
// Without this the final row of a window resumes into a window with nothing
|
|
332
406
|
// left in it, which is the blank page again one step later.
|
|
333
407
|
test('the last row of a page that closes its window names the next window', () => {
|
|
@@ -542,7 +616,7 @@ describe('queryByIndexSortFiltered', () => {
|
|
|
542
616
|
expect(result.query.expression).toBe('#ownerId = :ownerId')
|
|
543
617
|
expect(result.filter).toBeUndefined()
|
|
544
618
|
expect(result.index).toBe('ownerId')
|
|
545
|
-
expect(result.limit).toBe(
|
|
619
|
+
expect(result.limit).toBe(51)
|
|
546
620
|
})
|
|
547
621
|
|
|
548
622
|
test('query includes sortField when present', () => {
|
|
@@ -655,7 +729,8 @@ describe('invokeCommandGenerator', () => {
|
|
|
655
729
|
expect(payload.identity.userArn).toBe('arn:aws:iam::123:role/SyncRole')
|
|
656
730
|
expect(payload.identity.accountId).toBe('123')
|
|
657
731
|
expect(payload.identity.username).toBe('AROAEXAMPLE:caller')
|
|
658
|
-
|
|
732
|
+
// The caller's address is personal data and never reaches the command.
|
|
733
|
+
expect(payload.meta).not.toHaveProperty('ip')
|
|
659
734
|
expect(payload.meta.user).toBe('AROAEXAMPLE:caller')
|
|
660
735
|
})
|
|
661
736
|
|
|
@@ -663,7 +738,7 @@ describe('invokeCommandGenerator', () => {
|
|
|
663
738
|
const ctx = makeCtx({ identity: null })
|
|
664
739
|
const { payload } = request(ctx)
|
|
665
740
|
expect(payload.identity).toBeNull()
|
|
666
|
-
expect(payload.meta
|
|
741
|
+
expect(payload.meta).not.toHaveProperty('ip')
|
|
667
742
|
expect(payload.meta.user).toBeNull()
|
|
668
743
|
expect(payload.command).toBe('CreateProduct')
|
|
669
744
|
})
|
|
@@ -712,6 +787,28 @@ describe('invokeInboundTranslation', () => {
|
|
|
712
787
|
expect(result.payload.fieldName).toBe('importProduct')
|
|
713
788
|
expect(result.payload.arguments).toEqual({ csv: 'data' })
|
|
714
789
|
})
|
|
790
|
+
|
|
791
|
+
// The Lambda checks each translated command against the caller's groups.
|
|
792
|
+
test('request carries the Cognito caller and its groups', () => {
|
|
793
|
+
const ctx = makeCtx({
|
|
794
|
+
identity: { username: 'alice', sub: 's1', sourceIp: [], claims: { 'cognito:groups': ['Merchandiser'] } },
|
|
795
|
+
})
|
|
796
|
+
const { payload } = request(ctx)
|
|
797
|
+
expect(payload.identity.userId).toBe('s1')
|
|
798
|
+
expect(payload.identity.groups).toEqual(['Merchandiser'])
|
|
799
|
+
expect(payload.identity.provider).toBe('Cognito')
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
test('request carries an IAM caller as IAM', () => {
|
|
803
|
+
const ctx = makeCtx({ identity: { userArn: 'arn:aws:iam::123:role/Sync', accountId: '123' } })
|
|
804
|
+
const { payload } = request(ctx)
|
|
805
|
+
expect(payload.identity.provider).toBe('IAM')
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
test('request emits null identity when ctx.identity is null', () => {
|
|
809
|
+
const { payload } = request(makeCtx({ identity: null }))
|
|
810
|
+
expect(payload.identity).toBeNull()
|
|
811
|
+
})
|
|
715
812
|
})
|
|
716
813
|
|
|
717
814
|
// ---------------------------------------------------------------------------
|
|
@@ -907,11 +1004,11 @@ describe('listAllItemsConnection — owner scoping', () => {
|
|
|
907
1004
|
|
|
908
1005
|
// The read window exists because a FilterExpression cuts rows AFTER `Limit`.
|
|
909
1006
|
// A key condition does not, so a scoped caller with no filter of their own
|
|
910
|
-
// examines
|
|
911
|
-
// hiding, gone rather than papered over.
|
|
912
|
-
test('a scoped caller with no filter examines
|
|
1007
|
+
// examines the page they asked for and the one row that says whether another
|
|
1008
|
+
// follows — the arithmetic the window was hiding, gone rather than papered over.
|
|
1009
|
+
test('a scoped caller with no filter examines only the page, plus one', () => {
|
|
913
1010
|
const { request } = indexed()
|
|
914
|
-
expect(request(makeCtx({ args: { first: 25 }, identity: asUser('cust-a') })).limit).toBe(
|
|
1011
|
+
expect(request(makeCtx({ args: { first: 25 }, identity: asUser('cust-a') })).limit).toBe(26)
|
|
915
1012
|
})
|
|
916
1013
|
|
|
917
1014
|
test("a scoped caller's own filter still widens the window", () => {
|
|
@@ -1025,16 +1122,37 @@ describe('listAllItemsConnection — owner scoping', () => {
|
|
|
1025
1122
|
})
|
|
1026
1123
|
})
|
|
1027
1124
|
|
|
1028
|
-
// Backward paging
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1125
|
+
// Backward paging works the same way on both branches, and for the same reason:
|
|
1126
|
+
// it re-reads the window the cursor names and cuts the page ending at it, which
|
|
1127
|
+
// is operation-agnostic. `last` stays refused on both — it needs the end of the
|
|
1128
|
+
// list, which neither a Scan nor this Query's cursor can reach.
|
|
1031
1129
|
test.each([['scoped', asUser('cust-a')], ['exempt', asUser('ops-1', ['Admin'])]])(
|
|
1032
|
-
'a %s caller is still refused
|
|
1130
|
+
'a %s caller is still refused `last`',
|
|
1033
1131
|
(_, identity) => {
|
|
1034
1132
|
const { request } = indexed()
|
|
1035
|
-
expect(() => request(makeCtx({ args: { last: 5 }, identity }))).toThrow(
|
|
1133
|
+
expect(() => request(makeCtx({ args: { last: 5 }, identity }))).toThrow(
|
|
1134
|
+
'last is not supported',
|
|
1135
|
+
)
|
|
1036
1136
|
},
|
|
1037
1137
|
)
|
|
1138
|
+
|
|
1139
|
+
// The path tag has to survive the direction change: a backward cursor still
|
|
1140
|
+
// names the read that minted it, and replaying it on the other branch is the
|
|
1141
|
+
// same mistake as replaying a forward one.
|
|
1142
|
+
test('a backward cursor is path-checked like a forward one', () => {
|
|
1143
|
+
const { request, response } = indexed()
|
|
1144
|
+
const scopedPage = response(makeCtx({
|
|
1145
|
+
args: { first: 10 },
|
|
1146
|
+
identity: asUser('cust-a'),
|
|
1147
|
+
result: { items: Array.from({ length: 10 }, (_, i) => ({ id: 'r' + i })), nextToken: null },
|
|
1148
|
+
}))
|
|
1149
|
+
expect(() =>
|
|
1150
|
+
request(makeCtx({
|
|
1151
|
+
args: { first: 10, before: scopedPage.pageInfo.endCursor },
|
|
1152
|
+
identity: asUser('ops-1', ['Admin']),
|
|
1153
|
+
})),
|
|
1154
|
+
).toThrow('different read of this list')
|
|
1155
|
+
})
|
|
1038
1156
|
})
|
|
1039
1157
|
|
|
1040
1158
|
// ---------------------------------------------------------------------------
|