@reventlessdev/rescript-pulumi-aws 2.4.0-alpha.43
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 +366 -0
- package/LICENSE +202 -0
- package/README.md +19 -0
- package/package.json +52 -0
- package/rescript.json +27 -0
- package/scripts/bundle-resolvers.mjs +77 -0
- package/src/AWS/Aws_AvailabilityZone.res +9 -0
- package/src/AWS/Aws_AvailabilityZone.res.mjs +9 -0
- package/src/AWS/Aws_AvailabilityZone.resi +9 -0
- package/src/AWS/Aws_Aws.res +29 -0
- package/src/AWS/Aws_Aws.res.mjs +32 -0
- package/src/AWS/Aws_Common.res +2 -0
- package/src/AWS/Aws_Common.res.mjs +2 -0
- package/src/AWS/Aws_Provider.res +10 -0
- package/src/AWS/Aws_Provider.res.mjs +2 -0
- package/src/Acm/Acm.res +5 -0
- package/src/Acm/Acm.res.mjs +12 -0
- package/src/Acm/Acm_Certificate.res +27 -0
- package/src/Acm/Acm_Certificate.res.mjs +2 -0
- package/src/Acm/Acm_CertificateValidation.res +22 -0
- package/src/Acm/Acm_CertificateValidation.res.mjs +2 -0
- package/src/AppSync/AppSync.res +7 -0
- package/src/AppSync/AppSync.res.mjs +18 -0
- package/src/AppSync/AppSync_DataSource.res +93 -0
- package/src/AppSync/AppSync_DataSource.res.mjs +42 -0
- package/src/AppSync/AppSync_Function.res +75 -0
- package/src/AppSync/AppSync_Function.res.mjs +36 -0
- package/src/AppSync/AppSync_GraphQLApi.res +65 -0
- package/src/AppSync/AppSync_GraphQLApi.res.mjs +20 -0
- package/src/AppSync/AppSync_Resolver.res +229 -0
- package/src/AppSync/AppSync_Resolver.res.mjs +113 -0
- package/src/AppSync/AppSync_ResolverRuntime.res +214 -0
- package/src/AppSync/AppSync_ResolverRuntime.res.mjs +15 -0
- package/src/AppSync/AppSync_Resolver_Functions.res +998 -0
- package/src/AppSync/AppSync_Resolver_Functions.res.mjs +913 -0
- package/src/Aws.res +5 -0
- package/src/Aws.res.mjs +16 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_Api.res +92 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_Api.res.mjs +12 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_ChannelNamespace.res +81 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_ChannelNamespace.res.mjs +18 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_Resolver.res +39 -0
- package/src/AwsNative/AppSync/AwsNative_AppSync_Resolver.res.mjs +12 -0
- package/src/AwsNative/AwsNative.res +10 -0
- package/src/AwsNative/AwsNative.res.mjs +13 -0
- package/src/CloudFront/CloudFront.res +5 -0
- package/src/CloudFront/CloudFront.res.mjs +12 -0
- package/src/CloudFront/CloudFront_Distribution.res +81 -0
- package/src/CloudFront/CloudFront_Distribution.res.mjs +2 -0
- package/src/CloudFront/CloudFront_OriginAccessControl.res +20 -0
- package/src/CloudFront/CloudFront_OriginAccessControl.res.mjs +2 -0
- package/src/Cloudwatch/Cloudwatch.res +7 -0
- package/src/Cloudwatch/Cloudwatch.res.mjs +18 -0
- package/src/Cloudwatch/Cloudwatch_EventRule.res +52 -0
- package/src/Cloudwatch/Cloudwatch_EventRule.res.mjs +37 -0
- package/src/Cloudwatch/Cloudwatch_EventTarget.res +28 -0
- package/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs +25 -0
- package/src/Cloudwatch/Cloudwatch_LogGroup.res +14 -0
- package/src/Cloudwatch/Cloudwatch_LogGroup.res.mjs +2 -0
- package/src/Cloudwatch/Cloudwatch_LogMetricFilter.res +28 -0
- package/src/Cloudwatch/Cloudwatch_LogMetricFilter.res.mjs +2 -0
- package/src/Cognito/Cognito.res +8 -0
- package/src/Cognito/Cognito.res.mjs +21 -0
- package/src/Cognito/Cognito_IdentityPool.res +148 -0
- package/src/Cognito/Cognito_IdentityPool.res.mjs +65 -0
- package/src/Cognito/Cognito_IdentityPoolRoleAttachment.res +18 -0
- package/src/Cognito/Cognito_IdentityPoolRoleAttachment.res.mjs +2 -0
- package/src/Cognito/Cognito_UserGroup.res +16 -0
- package/src/Cognito/Cognito_UserGroup.res.mjs +2 -0
- package/src/Cognito/Cognito_UserPool.res +125 -0
- package/src/Cognito/Cognito_UserPool.res.mjs +44 -0
- package/src/Cognito/Cognito_UserPoolClient.res +33 -0
- package/src/Cognito/Cognito_UserPoolClient.res.mjs +2 -0
- package/src/DynamoDb/DynamoDb.res +5 -0
- package/src/DynamoDb/DynamoDb.res.mjs +12 -0
- package/src/DynamoDb/DynamoDb_Stream.res +41 -0
- package/src/DynamoDb/DynamoDb_Stream.res.mjs +2 -0
- package/src/DynamoDb/DynamoDb_Table.res +66 -0
- package/src/DynamoDb/DynamoDb_Table.res.mjs +2 -0
- package/src/EC2/EC2.res +13 -0
- package/src/EC2/EC2.res.mjs +36 -0
- package/src/EC2/EC2_Common.res +13 -0
- package/src/EC2/EC2_Common.res.mjs +22 -0
- package/src/EC2/EC2_DefaultSecurityGroup.res +22 -0
- package/src/EC2/EC2_DefaultSecurityGroup.res.mjs +19 -0
- package/src/EC2/EC2_Eip.res +24 -0
- package/src/EC2/EC2_Eip.res.mjs +18 -0
- package/src/EC2/EC2_InternetGateway.res +25 -0
- package/src/EC2/EC2_InternetGateway.res.mjs +18 -0
- package/src/EC2/EC2_NatGateway.res +30 -0
- package/src/EC2/EC2_NatGateway.res.mjs +18 -0
- package/src/EC2/EC2_RouteTable.res +49 -0
- package/src/EC2/EC2_RouteTable.res.mjs +38 -0
- package/src/EC2/EC2_RouteTableAssociation.res +30 -0
- package/src/EC2/EC2_RouteTableAssociation.res.mjs +18 -0
- package/src/EC2/EC2_SecurityGroup.res +44 -0
- package/src/EC2/EC2_SecurityGroup.res.mjs +49 -0
- package/src/EC2/EC2_Subnet.res +31 -0
- package/src/EC2/EC2_Subnet.res.mjs +19 -0
- package/src/EC2/EC2_Vpc.res +25 -0
- package/src/EC2/EC2_Vpc.res.mjs +18 -0
- package/src/EC2/EC2_VpcEndpoint.res +43 -0
- package/src/EC2/EC2_VpcEndpoint.res.mjs +22 -0
- package/src/ECS/ECS.res +6 -0
- package/src/ECS/ECS.res.mjs +15 -0
- package/src/ECS/ECS_Cluster.res +10 -0
- package/src/ECS/ECS_Cluster.res.mjs +2 -0
- package/src/ECS/ECS_Container.res +58 -0
- package/src/ECS/ECS_Container.res.mjs +2 -0
- package/src/ECS/ECS_TaskDefinition.res +31 -0
- package/src/ECS/ECS_TaskDefinition.res.mjs +2 -0
- package/src/IAM/IAM.res +148 -0
- package/src/IAM/IAM.res.mjs +61 -0
- package/src/IAM/IAM_ManagedPolicies.res +32 -0
- package/src/IAM/IAM_ManagedPolicies.res.mjs +2 -0
- package/src/IAM/PolicyDocument.res +163 -0
- package/src/IAM/PolicyDocument.res.mjs +164 -0
- package/src/Kinesis/Kinesis.res +4 -0
- package/src/Kinesis/Kinesis.res.mjs +9 -0
- package/src/Kinesis/Kinesis_Stream.res +70 -0
- package/src/Kinesis/Kinesis_Stream.res.mjs +9 -0
- package/src/Lambda/EventSourceMapping.res +78 -0
- package/src/Lambda/EventSourceMapping.res.mjs +2 -0
- package/src/Lambda/FunctionUrl.res +41 -0
- package/src/Lambda/FunctionUrl.res.mjs +2 -0
- package/src/Lambda/Lambda.res +270 -0
- package/src/Lambda/Lambda.res.mjs +94 -0
- package/src/Route53/Route53.res +4 -0
- package/src/Route53/Route53.res.mjs +9 -0
- package/src/Route53/Route53_Record.res +36 -0
- package/src/Route53/Route53_Record.res.mjs +2 -0
- package/src/S3/S3.res +10 -0
- package/src/S3/S3.res.mjs +27 -0
- package/src/S3/S3_Bucket.res +236 -0
- package/src/S3/S3_Bucket.res.mjs +2 -0
- package/src/S3/S3_BucketAclV2.res +24 -0
- package/src/S3/S3_BucketAclV2.res.mjs +2 -0
- package/src/S3/S3_BucketObject.res +22 -0
- package/src/S3/S3_BucketObject.res.mjs +2 -0
- package/src/S3/S3_BucketOwnershipControls.res +19 -0
- package/src/S3/S3_BucketOwnershipControls.res.mjs +2 -0
- package/src/S3/S3_BucketPolicy.res +13 -0
- package/src/S3/S3_BucketPolicy.res.mjs +2 -0
- package/src/S3/S3_BucketPublicAccessBlock.res +16 -0
- package/src/S3/S3_BucketPublicAccessBlock.res.mjs +2 -0
- package/src/S3/S3_BucketV2.res +11 -0
- package/src/S3/S3_BucketV2.res.mjs +2 -0
- package/src/SES/SES.bs.js +11 -0
- package/src/SES/SES.res +25 -0
- package/src/SES/SES.res.mjs +12 -0
- package/src/SNS/SNS.res +5 -0
- package/src/SNS/SNS.res.mjs +12 -0
- package/src/SNS/SNS_Topic.res +71 -0
- package/src/SNS/SNS_Topic.res.mjs +2 -0
- package/src/SNS/SNS_TopicSubscription.res +21 -0
- package/src/SNS/SNS_TopicSubscription.res.mjs +2 -0
- package/src/SQS/SQS.res +5 -0
- package/src/SQS/SQS.res.mjs +12 -0
- package/src/SQS/SQS_Queue.res +92 -0
- package/src/SQS/SQS_Queue.res.mjs +19 -0
- package/src/SQS/SQS_QueuePolicy.res +13 -0
- package/src/SQS/SQS_QueuePolicy.res.mjs +2 -0
- package/src/SecretsManager/GetSecretVersion.res +37 -0
- package/src/SecretsManager/GetSecretVersion.res.mjs +23 -0
- package/src/example/CustomDomainExample.res +71 -0
- package/src/example/CustomDomainExample.res.mjs +57 -0
- package/tests/AppSync_Resolver_FunctionsTest.mjs +597 -0
- package/tests/__mocks__/appsync-utils.mjs +26 -0
- package/tests/resolverTestHelper.mjs +46 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { evalResolver, makeCtx } from './resolverTestHelper.mjs'
|
|
2
|
+
import * as F from '../src/AppSync/AppSync_Resolver_Functions.res.mjs'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helper: all resolver code should include the appsync utils import
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const hasUtilImport = code => code.includes("import { util } from '@aws-appsync/utils'")
|
|
8
|
+
const hasRequestFn = code => code.includes('export function request(') || code.includes('__exports.request')
|
|
9
|
+
const hasResponseFn = code => code.includes('export function response(') || code.includes('__exports.response')
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Structure: every exported code string must be a valid resolver module
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
describe('Resolver code structure', () => {
|
|
15
|
+
// Labeled ReScript args compile to positional JS args in declaration order.
|
|
16
|
+
const codeValues = [
|
|
17
|
+
['pipelinePassThrough', F.pipelinePassThrough],
|
|
18
|
+
['getItemById', F.getItemById],
|
|
19
|
+
['queryById', F.queryById],
|
|
20
|
+
['queryByIdSort(sortField)', F.queryByIdSort('status')],
|
|
21
|
+
['queryByIndex(index)', F.queryByIndex('userId')],
|
|
22
|
+
['queryByIndexDeletable(index)', F.queryByIndexDeletable('userId')],
|
|
23
|
+
['queryByIndexSort(index,idField,sortField)', F.queryByIndexSort('userId', 'userId', 'createdAt')],
|
|
24
|
+
['queryByIndexFiltered(index,idField)', F.queryByIndexFiltered('userId', 'userId')],
|
|
25
|
+
['queryByIndexSortFiltered(index,idField,sortField)', F.queryByIndexSortFiltered('userId', 'userId', 'status')],
|
|
26
|
+
['listAllItems', F.listAllItems],
|
|
27
|
+
['resolveId(sourceIdField)', F.resolveId('productId')],
|
|
28
|
+
['resolveIdSort(sourceIdField,sourceSortField,targetSortField)', F.resolveIdSort('productId', 'status', 'status')],
|
|
29
|
+
['resolveIdByIndex(index,sourceIdField,targetIdField)', F.resolveIdByIndex('productId', 'productId', 'productId')],
|
|
30
|
+
['putItem', F.putItem],
|
|
31
|
+
['addItemToList(listName,itemName)', F.addItemToList('items', 'itemId')],
|
|
32
|
+
['deleteItem', F.deleteItem],
|
|
33
|
+
['invokeCommandGenerator(cmd)', F.invokeCommandGenerator('CreateProduct')],
|
|
34
|
+
['invokeDcbMutation(tag)', F.invokeDcbMutation('Product')],
|
|
35
|
+
['invokeInboundTranslation(field)', F.invokeInboundTranslation('importProduct')],
|
|
36
|
+
['authorizeIndexedAccess(index,group)', F.authorizeIndexedAccess('userId', 'Admin')],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
test.each(codeValues)('%s includes util import', (_, code) => {
|
|
40
|
+
expect(hasUtilImport(code)).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test.each(codeValues)('%s exports request function', (_, code) => {
|
|
44
|
+
expect(hasRequestFn(code)).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test.each(codeValues)('%s exports response function', (_, code) => {
|
|
48
|
+
expect(hasResponseFn(code)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// getItemById
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
describe('getItemById', () => {
|
|
56
|
+
const { request, response } = evalResolver(F.getItemById)
|
|
57
|
+
|
|
58
|
+
test('request returns GetItem with id key', () => {
|
|
59
|
+
const ctx = makeCtx({ args: { id: 'abc123' } })
|
|
60
|
+
const result = request(ctx)
|
|
61
|
+
expect(result.operation).toBe('GetItem')
|
|
62
|
+
expect(result.key.id).toEqual({ S: 'abc123' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('response returns ctx.result on success', () => {
|
|
66
|
+
const ctx = makeCtx({ result: { id: 'abc123', name: 'Widget' } })
|
|
67
|
+
expect(response(ctx)).toEqual({ id: 'abc123', name: 'Widget' })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('response throws on ctx.error', () => {
|
|
71
|
+
const ctx = makeCtx({ error: { message: 'not found', type: 'NotFound' } })
|
|
72
|
+
expect(() => response(ctx)).toThrow('not found')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// queryById
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
describe('queryById', () => {
|
|
80
|
+
const { request, response } = evalResolver(F.queryById)
|
|
81
|
+
|
|
82
|
+
test('request returns Query by id', () => {
|
|
83
|
+
const ctx = makeCtx({ args: { id: 'xyz' } })
|
|
84
|
+
const result = request(ctx)
|
|
85
|
+
expect(result.operation).toBe('Query')
|
|
86
|
+
expect(result.query.expression).toBe('id = :id')
|
|
87
|
+
expect(result.query.expressionValues[':id']).toEqual({ S: 'xyz' })
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// queryByIdSort
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
describe('queryByIdSort', () => {
|
|
95
|
+
const code = F.queryByIdSort('status') // single positional arg
|
|
96
|
+
const { request } = evalResolver(code)
|
|
97
|
+
|
|
98
|
+
test('request queries by id and status', () => {
|
|
99
|
+
const ctx = makeCtx({ args: { id: 'p1', status: 'active' } })
|
|
100
|
+
const result = request(ctx)
|
|
101
|
+
expect(result.operation).toBe('Query')
|
|
102
|
+
expect(result.query.expression).toContain('id = :id')
|
|
103
|
+
expect(result.query.expression).toContain('#status = :status')
|
|
104
|
+
expect(result.query.expressionValues[':id']).toEqual({ S: 'p1' })
|
|
105
|
+
expect(result.query.expressionValues[':status']).toEqual({ S: 'active' })
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// queryByIndex
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
describe('queryByIndex', () => {
|
|
113
|
+
const { request, response } = evalResolver(F.queryByIndex('userId'))
|
|
114
|
+
|
|
115
|
+
test('request queries by userId index', () => {
|
|
116
|
+
const ctx = makeCtx({ args: { userId: 'u1' } })
|
|
117
|
+
const result = request(ctx)
|
|
118
|
+
expect(result.operation).toBe('Query')
|
|
119
|
+
expect(result.index).toBe('userId')
|
|
120
|
+
expect(result.query.expressionValues[':userId']).toEqual({ S: 'u1' })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('response returns ctx.result', () => {
|
|
124
|
+
const ctx = makeCtx({ result: [{ id: 'a' }] })
|
|
125
|
+
expect(response(ctx)).toEqual([{ id: 'a' }])
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// listAllItems
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
describe('listAllItems', () => {
|
|
133
|
+
const { request } = evalResolver(F.listAllItems)
|
|
134
|
+
|
|
135
|
+
test('request returns Scan with default limit 50', () => {
|
|
136
|
+
const ctx = makeCtx({ args: {} })
|
|
137
|
+
const result = request(ctx)
|
|
138
|
+
expect(result.operation).toBe('Scan')
|
|
139
|
+
expect(result.limit).toBe(50)
|
|
140
|
+
expect(result.nextToken).toBeNull()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('request respects explicit limit', () => {
|
|
144
|
+
const ctx = makeCtx({ args: { limit: 10 } })
|
|
145
|
+
expect(request(ctx).limit).toBe(10)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// listAllItemsConnection — Phase 3 server-side filter/sort
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
describe('listAllItemsConnection', () => {
|
|
153
|
+
describe('default (no capability args)', () => {
|
|
154
|
+
const { request, response } = evalResolver(F.listAllItemsConnection('name'))
|
|
155
|
+
|
|
156
|
+
test('request returns Scan without filter when no args', () => {
|
|
157
|
+
const ctx = makeCtx({ args: {} })
|
|
158
|
+
const result = request(ctx)
|
|
159
|
+
expect(result.operation).toBe('Scan')
|
|
160
|
+
expect(result.limit).toBe(50)
|
|
161
|
+
expect(result.nextToken).toBeNull()
|
|
162
|
+
expect(result.filter).toBeUndefined()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('request adds search clause as contains(labelField)', () => {
|
|
166
|
+
const ctx = makeCtx({ args: { filter: { search: 'Wid' } } })
|
|
167
|
+
const result = request(ctx)
|
|
168
|
+
expect(result.filter.expression).toBe('contains(#label, :search)')
|
|
169
|
+
expect(result.filter.expressionNames['#label']).toBe('name')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('request adds searchPrefix as begins_with(labelField)', () => {
|
|
173
|
+
const ctx = makeCtx({ args: { filter: { searchPrefix: 'Wid' } } })
|
|
174
|
+
const result = request(ctx)
|
|
175
|
+
expect(result.filter.expression).toBe('begins_with(#label, :searchPrefix)')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('request adds ids as IN clause', () => {
|
|
179
|
+
const ctx = makeCtx({ args: { filter: { ids: ['a', 'b'] } } })
|
|
180
|
+
const result = request(ctx)
|
|
181
|
+
expect(result.filter.expression).toBe('#id IN (:id0, :id1)')
|
|
182
|
+
expect(result.filter.expressionValues[':id0']).toEqual({ S: 'a' })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('response returns edges + pageInfo with positional cursors', () => {
|
|
186
|
+
const ctx = makeCtx({
|
|
187
|
+
args: {},
|
|
188
|
+
result: { items: [{ id: 'a' }, { id: 'b' }], nextToken: 'next' },
|
|
189
|
+
})
|
|
190
|
+
const r = response(ctx)
|
|
191
|
+
expect(r.edges).toHaveLength(2)
|
|
192
|
+
expect(r.edges[0].node).toEqual({ id: 'a' })
|
|
193
|
+
expect(r.pageInfo.hasNextPage).toBe(true)
|
|
194
|
+
expect(r.pageInfo.hasPreviousPage).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('with filterFields (per-field Eq)', () => {
|
|
199
|
+
const code = F.listAllItemsConnection('name', ['status', 'ownerId'])
|
|
200
|
+
const { request } = evalResolver(code)
|
|
201
|
+
|
|
202
|
+
test('request adds status = :statusEq when filter.statusEq is set', () => {
|
|
203
|
+
const ctx = makeCtx({ args: { filter: { statusEq: 'active' } } })
|
|
204
|
+
const result = request(ctx)
|
|
205
|
+
expect(result.filter.expression).toBe('#status = :statusEq')
|
|
206
|
+
expect(result.filter.expressionNames['#status']).toBe('status')
|
|
207
|
+
expect(result.filter.expressionValues[':statusEq']).toEqual({ S: 'active' })
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('request combines multiple per-field eq with AND', () => {
|
|
211
|
+
const ctx = makeCtx({
|
|
212
|
+
args: { filter: { statusEq: 'active', ownerIdEq: 'u1' } },
|
|
213
|
+
})
|
|
214
|
+
const result = request(ctx)
|
|
215
|
+
expect(result.filter.expression).toBe('#status = :statusEq AND #ownerId = :ownerIdEq')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('request omits clause when value is null/undefined/empty', () => {
|
|
219
|
+
const ctx = makeCtx({ args: { filter: { statusEq: '', ownerIdEq: null } } })
|
|
220
|
+
const result = request(ctx)
|
|
221
|
+
expect(result.filter).toBeUndefined()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('request combines per-field eq with legacy search', () => {
|
|
225
|
+
const ctx = makeCtx({
|
|
226
|
+
args: { filter: { search: 'Wid', statusEq: 'active' } },
|
|
227
|
+
})
|
|
228
|
+
const result = request(ctx)
|
|
229
|
+
expect(result.filter.expression).toBe(
|
|
230
|
+
'contains(#label, :search) AND #status = :statusEq',
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('with rangeFields (per-field From/To)', () => {
|
|
236
|
+
const code = F.listAllItemsConnection('name', ['createdAt'], ['createdAt'])
|
|
237
|
+
const { request } = evalResolver(code)
|
|
238
|
+
|
|
239
|
+
test('request adds >= clause for From', () => {
|
|
240
|
+
const ctx = makeCtx({ args: { filter: { createdAtFrom: '2026-01-01' } } })
|
|
241
|
+
const result = request(ctx)
|
|
242
|
+
expect(result.filter.expression).toBe('#createdAt >= :createdAtFrom')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('request adds <= clause for To', () => {
|
|
246
|
+
const ctx = makeCtx({ args: { filter: { createdAtTo: '2026-12-31' } } })
|
|
247
|
+
const result = request(ctx)
|
|
248
|
+
expect(result.filter.expression).toBe('#createdAt <= :createdAtTo')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('request combines From + To with AND', () => {
|
|
252
|
+
const ctx = makeCtx({
|
|
253
|
+
args: {
|
|
254
|
+
filter: { createdAtFrom: '2026-01-01', createdAtTo: '2026-12-31' },
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
const result = request(ctx)
|
|
258
|
+
expect(result.filter.expression).toBe(
|
|
259
|
+
'#createdAt >= :createdAtFrom AND #createdAt <= :createdAtTo',
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('eq + range can compose on the same field', () => {
|
|
264
|
+
const ctx = makeCtx({
|
|
265
|
+
args: { filter: { createdAtEq: '2026-06-15', createdAtFrom: '2026-01-01' } },
|
|
266
|
+
})
|
|
267
|
+
const result = request(ctx)
|
|
268
|
+
expect(result.filter.expression).toBe(
|
|
269
|
+
'#createdAt = :createdAtEq AND #createdAt >= :createdAtFrom',
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('with sortFields (per-page sort)', () => {
|
|
275
|
+
const code = F.listAllItemsConnection('name', [], [], ['name', 'createdAt'])
|
|
276
|
+
const { response } = evalResolver(code)
|
|
277
|
+
|
|
278
|
+
test('response sorts items ASC by orderBy.field', () => {
|
|
279
|
+
const ctx = makeCtx({
|
|
280
|
+
args: { orderBy: { field: 'name', direction: 'ASC' } },
|
|
281
|
+
result: {
|
|
282
|
+
items: [{ name: 'C' }, { name: 'A' }, { name: 'B' }],
|
|
283
|
+
nextToken: null,
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
const r = response(ctx)
|
|
287
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['A', 'B', 'C'])
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('response sorts items DESC by orderBy.field', () => {
|
|
291
|
+
const ctx = makeCtx({
|
|
292
|
+
args: { orderBy: { field: 'name', direction: 'DESC' } },
|
|
293
|
+
result: {
|
|
294
|
+
items: [{ name: 'A' }, { name: 'C' }, { name: 'B' }],
|
|
295
|
+
nextToken: null,
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
const r = response(ctx)
|
|
299
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['C', 'B', 'A'])
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('response leaves order untouched when orderBy.field is not in sortFields', () => {
|
|
303
|
+
const ctx = makeCtx({
|
|
304
|
+
args: { orderBy: { field: 'unknown', direction: 'ASC' } },
|
|
305
|
+
result: { items: [{ name: 'C' }, { name: 'A' }], nextToken: null },
|
|
306
|
+
})
|
|
307
|
+
const r = response(ctx)
|
|
308
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['C', 'A'])
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('response leaves order untouched when no orderBy supplied', () => {
|
|
312
|
+
const ctx = makeCtx({
|
|
313
|
+
args: {},
|
|
314
|
+
result: { items: [{ name: 'C' }, { name: 'A' }], nextToken: null },
|
|
315
|
+
})
|
|
316
|
+
const r = response(ctx)
|
|
317
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['C', 'A'])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('response sorts null/undefined values to the end', () => {
|
|
321
|
+
const ctx = makeCtx({
|
|
322
|
+
args: { orderBy: { field: 'name', direction: 'ASC' } },
|
|
323
|
+
result: {
|
|
324
|
+
items: [{ name: 'B' }, { name: null }, { name: 'A' }],
|
|
325
|
+
nextToken: null,
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
const r = response(ctx)
|
|
329
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['A', 'B', null])
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('response keeps nulls at the end under DESC direction', () => {
|
|
333
|
+
const ctx = makeCtx({
|
|
334
|
+
args: { orderBy: { field: 'name', direction: 'DESC' } },
|
|
335
|
+
result: {
|
|
336
|
+
items: [{ name: 'A' }, { name: null }, { name: 'B' }],
|
|
337
|
+
nextToken: null,
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
const r = response(ctx)
|
|
341
|
+
expect(r.edges.map(e => e.node.name)).toEqual(['B', 'A', null])
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('response sorts numeric fields in numeric (not lexicographic) order', () => {
|
|
345
|
+
// The schwartzian transform pads numbers so '10' sorts after '2', not before.
|
|
346
|
+
const code = F.listAllItemsConnection('name', [], [], ['count'])
|
|
347
|
+
const { response: numResp } = evalResolver(code)
|
|
348
|
+
const ctx = makeCtx({
|
|
349
|
+
args: { orderBy: { field: 'count', direction: 'ASC' } },
|
|
350
|
+
result: {
|
|
351
|
+
items: [{ count: 10 }, { count: 2 }, { count: 100 }],
|
|
352
|
+
nextToken: null,
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
const r = numResp(ctx)
|
|
356
|
+
expect(r.edges.map(e => e.node.count)).toEqual([2, 10, 100])
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('combined filter + sort', () => {
|
|
361
|
+
const code = F.listAllItemsConnection('name', ['status'], [], ['name'])
|
|
362
|
+
const { request, response } = evalResolver(code)
|
|
363
|
+
|
|
364
|
+
test('request emits filter; response sorts page', () => {
|
|
365
|
+
const reqCtx = makeCtx({
|
|
366
|
+
args: {
|
|
367
|
+
filter: { statusEq: 'active' },
|
|
368
|
+
orderBy: { field: 'name', direction: 'ASC' },
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
expect(request(reqCtx).filter.expression).toBe('#status = :statusEq')
|
|
372
|
+
const respCtx = makeCtx({
|
|
373
|
+
args: { orderBy: { field: 'name', direction: 'ASC' } },
|
|
374
|
+
result: { items: [{ name: 'B' }, { name: 'A' }], nextToken: null },
|
|
375
|
+
})
|
|
376
|
+
expect(response(respCtx).edges.map(e => e.node.name)).toEqual(['A', 'B'])
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// queryByIndexSortFiltered — complex template, most important to test
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
describe('queryByIndexSortFiltered', () => {
|
|
385
|
+
const code = F.queryByIndexSortFiltered('ownerId', 'ownerId', 'status') // index, idField, sortField
|
|
386
|
+
const { request, response } = evalResolver(code)
|
|
387
|
+
|
|
388
|
+
test('query by idField only when sortField is absent', () => {
|
|
389
|
+
const ctx = makeCtx({ args: { ownerId: 'u1' } })
|
|
390
|
+
const result = request(ctx)
|
|
391
|
+
expect(result.operation).toBe('Query')
|
|
392
|
+
expect(result.query.expression).toBe('#ownerId = :ownerId')
|
|
393
|
+
expect(result.filter).toBeUndefined()
|
|
394
|
+
expect(result.index).toBe('ownerId')
|
|
395
|
+
expect(result.limit).toBe(50)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('query includes sortField when present', () => {
|
|
399
|
+
const ctx = makeCtx({ args: { ownerId: 'u1', status: 'active' } })
|
|
400
|
+
const result = request(ctx)
|
|
401
|
+
expect(result.query.expression).toContain('#status = :status')
|
|
402
|
+
expect(result.filter).toBeUndefined()
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('hideDeleted adds filter expression', () => {
|
|
406
|
+
const ctx = makeCtx({ args: { ownerId: 'u1', hideDeleted: true } })
|
|
407
|
+
const result = request(ctx)
|
|
408
|
+
expect(result.filter).toBeDefined()
|
|
409
|
+
expect(result.filter.expression).toContain('#deleted = :false')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('array arg adds contains filter for each value', () => {
|
|
413
|
+
const ctx = makeCtx({ args: { ownerId: 'u1', tags: ['a', 'b'] } })
|
|
414
|
+
const result = request(ctx)
|
|
415
|
+
expect(result.filter).toBeDefined()
|
|
416
|
+
expect(result.filter.expression).toContain('contains(#tags')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('string arg adds contains filter', () => {
|
|
420
|
+
const ctx = makeCtx({ args: { ownerId: 'u1', name: 'Widget' } })
|
|
421
|
+
const result = request(ctx)
|
|
422
|
+
expect(result.filter).toBeDefined()
|
|
423
|
+
expect(result.filter.expression).toContain('contains(#name, :name)')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('scanIndexForward defaults to true', () => {
|
|
427
|
+
const ctx = makeCtx({ args: { ownerId: 'u1' } })
|
|
428
|
+
expect(request(ctx).scanIndexForward).toBe(true)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('response returns result on success', () => {
|
|
432
|
+
const ctx = makeCtx({ result: { items: [{ id: 'x' }] } })
|
|
433
|
+
expect(response(ctx)).toEqual({ items: [{ id: 'x' }] })
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// putItem / deleteItem
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
describe('putItem', () => {
|
|
441
|
+
const { request } = evalResolver(F.putItem)
|
|
442
|
+
|
|
443
|
+
test('request returns PutItem with key and attributeValues', () => {
|
|
444
|
+
const ctx = makeCtx({ args: { id: 'p1', name: 'Widget' } })
|
|
445
|
+
const result = request(ctx)
|
|
446
|
+
expect(result.operation).toBe('PutItem')
|
|
447
|
+
expect(result.key.id).toEqual({ S: 'p1' })
|
|
448
|
+
expect(result.attributeValues).toBeDefined()
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
describe('deleteItem', () => {
|
|
453
|
+
const { request } = evalResolver(F.deleteItem)
|
|
454
|
+
|
|
455
|
+
test('request returns DeleteItem with string key', () => {
|
|
456
|
+
const ctx = makeCtx({ args: { id: 'p1' } })
|
|
457
|
+
const result = request(ctx)
|
|
458
|
+
expect(result.operation).toBe('DeleteItem')
|
|
459
|
+
expect(result.key.id).toEqual({ S: 'p1' })
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Lambda invocations
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
describe('invokeCommandGenerator', () => {
|
|
467
|
+
const { request, response } = evalResolver(F.invokeCommandGenerator('CreateProduct'))
|
|
468
|
+
|
|
469
|
+
test('request sets operation Invoke with command CreateProduct', () => {
|
|
470
|
+
const ctx = makeCtx({ args: { name: 'Widget' } })
|
|
471
|
+
const result = request(ctx)
|
|
472
|
+
expect(result.operation).toBe('Invoke')
|
|
473
|
+
expect(result.payload.command).toBe('CreateProduct')
|
|
474
|
+
expect(result.payload.arguments).toEqual({ name: 'Widget' })
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
test('request includes Cognito identity', () => {
|
|
478
|
+
const ctx = makeCtx()
|
|
479
|
+
const { payload } = request(ctx)
|
|
480
|
+
expect(payload.identity.provider).toBe('Cognito')
|
|
481
|
+
expect(payload.identity.username).toBe('testuser')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('request emits IAM identity when ctx.identity has userArn but no sub', () => {
|
|
485
|
+
const ctx = makeCtx({
|
|
486
|
+
identity: {
|
|
487
|
+
userArn: 'arn:aws:iam::123:role/SyncRole',
|
|
488
|
+
accountId: '123',
|
|
489
|
+
username: 'AROAEXAMPLE:caller',
|
|
490
|
+
sourceIp: ['10.0.0.1'],
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
const { payload } = request(ctx)
|
|
494
|
+
expect(payload.identity.provider).toBe('IAM')
|
|
495
|
+
expect(payload.identity.userArn).toBe('arn:aws:iam::123:role/SyncRole')
|
|
496
|
+
expect(payload.identity.accountId).toBe('123')
|
|
497
|
+
expect(payload.identity.username).toBe('AROAEXAMPLE:caller')
|
|
498
|
+
expect(payload.meta.ip).toEqual(['10.0.0.1'])
|
|
499
|
+
expect(payload.meta.user).toBe('AROAEXAMPLE:caller')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('request emits null identity when ctx.identity is null (API_KEY)', () => {
|
|
503
|
+
const ctx = makeCtx({ identity: null })
|
|
504
|
+
const { payload } = request(ctx)
|
|
505
|
+
expect(payload.identity).toBeNull()
|
|
506
|
+
expect(payload.meta.ip).toBeNull()
|
|
507
|
+
expect(payload.meta.user).toBeNull()
|
|
508
|
+
expect(payload.command).toBe('CreateProduct')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('response returns ctx.result', () => {
|
|
512
|
+
const ctx = makeCtx({ result: { msgId: '42' } })
|
|
513
|
+
expect(response(ctx)).toEqual({ msgId: '42' })
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
describe('invokeDcbMutation', () => {
|
|
518
|
+
const { request } = evalResolver(F.invokeDcbMutation('Product'))
|
|
519
|
+
|
|
520
|
+
test('request emits Cognito identity by default', () => {
|
|
521
|
+
const ctx = makeCtx({ args: { sku: 'X1' } })
|
|
522
|
+
const { payload } = request(ctx)
|
|
523
|
+
expect(payload.command).toBe('Product')
|
|
524
|
+
expect(payload.arguments).toEqual({ sku: 'X1' })
|
|
525
|
+
expect(payload.identity.provider).toBe('Cognito')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test('request emits IAM identity when ctx.identity has userArn but no sub', () => {
|
|
529
|
+
const ctx = makeCtx({
|
|
530
|
+
identity: { userArn: 'arn:aws:iam::123:role/Sync', accountId: '123' },
|
|
531
|
+
})
|
|
532
|
+
const { payload } = request(ctx)
|
|
533
|
+
expect(payload.identity.provider).toBe('IAM')
|
|
534
|
+
expect(payload.identity.userArn).toBe('arn:aws:iam::123:role/Sync')
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
test('request emits null identity when ctx.identity is null', () => {
|
|
538
|
+
const ctx = makeCtx({ identity: null })
|
|
539
|
+
const { payload } = request(ctx)
|
|
540
|
+
expect(payload.identity).toBeNull()
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
describe('invokeInboundTranslation', () => {
|
|
545
|
+
const { request } = evalResolver(F.invokeInboundTranslation('importProduct'))
|
|
546
|
+
|
|
547
|
+
test('request sets __inboundTranslation flag and fieldName', () => {
|
|
548
|
+
const ctx = makeCtx({ args: { csv: 'data' } })
|
|
549
|
+
const result = request(ctx)
|
|
550
|
+
expect(result.operation).toBe('Invoke')
|
|
551
|
+
expect(result.payload.__inboundTranslation).toBe(true)
|
|
552
|
+
expect(result.payload.fieldName).toBe('importProduct')
|
|
553
|
+
expect(result.payload.arguments).toEqual({ csv: 'data' })
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Authorization
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
describe('authorizeIndexedAccess', () => {
|
|
561
|
+
const code = F.authorizeIndexedAccess('resourceId', 'Admin') // index, group
|
|
562
|
+
const { request, response } = evalResolver(code)
|
|
563
|
+
|
|
564
|
+
test('request does GetItem when user is in Admin group', () => {
|
|
565
|
+
const ctx = makeCtx({
|
|
566
|
+
args: { resourceId: 'r1' },
|
|
567
|
+
identity: { username: 'alice', sub: 's1', sourceIp: [], claims: { 'cognito:groups': ['Admin'] } },
|
|
568
|
+
})
|
|
569
|
+
const result = request(ctx)
|
|
570
|
+
expect(result.operation).toBe('GetItem')
|
|
571
|
+
expect(result.key.id).toEqual({ S: 'r1' })
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
test('request earlyReturns when user is not in Admin group', () => {
|
|
575
|
+
const ctx = makeCtx({
|
|
576
|
+
args: { resourceId: 'r1' },
|
|
577
|
+
identity: { username: 'bob', sub: 's2', sourceIp: [], claims: { 'cognito:groups': ['User'] } },
|
|
578
|
+
})
|
|
579
|
+
expect(() => request(ctx)).toThrow('earlyReturn')
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('response throws unauthorized when adminId does not match username', () => {
|
|
583
|
+
const ctx = makeCtx({
|
|
584
|
+
result: { adminId: 'alice' },
|
|
585
|
+
identity: { username: 'bob', sub: 's2', sourceIp: [], claims: {} },
|
|
586
|
+
})
|
|
587
|
+
expect(() => response(ctx)).toThrow('Unauthorized')
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
test('response returns result when adminId matches username', () => {
|
|
591
|
+
const ctx = makeCtx({
|
|
592
|
+
result: { adminId: 'alice', data: 'secret' },
|
|
593
|
+
identity: { username: 'alice', sub: 's1', sourceIp: [], claims: {} },
|
|
594
|
+
})
|
|
595
|
+
expect(response(ctx)).toEqual({ adminId: 'alice', data: 'secret' })
|
|
596
|
+
})
|
|
597
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Minimal mock of @aws-appsync/utils for unit testing resolver functions. */
|
|
2
|
+
|
|
3
|
+
export const util = {
|
|
4
|
+
dynamodb: {
|
|
5
|
+
toDynamoDB: val => ({ S: String(val) }),
|
|
6
|
+
toString: val => ({ S: String(val) }),
|
|
7
|
+
toNumber: val => ({ N: String(val) }),
|
|
8
|
+
toBoolean: val => ({ BOOL: val }),
|
|
9
|
+
toNull: () => ({ NULL: true }),
|
|
10
|
+
toList: items => ({ L: items }),
|
|
11
|
+
toMapValues: obj => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, { S: String(v) }])),
|
|
12
|
+
toStringSet: arr => ({ SS: arr }),
|
|
13
|
+
toNumberSet: arr => ({ NS: arr.map(String) }),
|
|
14
|
+
},
|
|
15
|
+
error: (msg, type) => { throw Object.assign(new Error(msg), { errorType: type }) },
|
|
16
|
+
unauthorized: () => { throw new Error('Unauthorized') },
|
|
17
|
+
defaultIfNull: (val, def) => (val === null || val === undefined ? def : val),
|
|
18
|
+
defaultIfNullOrBlank: (val, def) => (!val || (typeof val === 'string' && val.trim() === '') ? def : val),
|
|
19
|
+
isNull: val => val === null || val === undefined,
|
|
20
|
+
isNullOrBlank: val => !val || (typeof val === 'string' && val.trim() === ''),
|
|
21
|
+
isList: val => Array.isArray(val),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const runtime = {
|
|
25
|
+
earlyReturn: val => { throw Object.assign(new Error('earlyReturn'), { earlyReturnValue: val }) },
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluates an AppSync JS resolver code string and returns the exported
|
|
3
|
+
* { request, response } functions, with @aws-appsync/utils mocked.
|
|
4
|
+
*
|
|
5
|
+
* Approach: strip the import statement (it's mocked via Jest moduleNameMapper
|
|
6
|
+
* when importing from .res.mjs files, but here we inline the mock) and
|
|
7
|
+
* evaluate the code with the mock injected into scope via Function constructor.
|
|
8
|
+
*/
|
|
9
|
+
import * as appsyncUtils from './__mocks__/appsync-utils.mjs'
|
|
10
|
+
|
|
11
|
+
const { util, runtime } = appsyncUtils
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a resolver code string into callable { request, response } functions.
|
|
15
|
+
* Strips ES module syntax (import/export) and evaluates with mocked utils.
|
|
16
|
+
*/
|
|
17
|
+
export function evalResolver(code) {
|
|
18
|
+
// Strip import statements (mocked externally)
|
|
19
|
+
let stripped = code.replace(/^import\s+.*?from\s+['"][^'"]+['"];?\s*/gm, '')
|
|
20
|
+
// Replace `export function X` with `__exports.X = function X`
|
|
21
|
+
stripped = stripped.replace(/export\s+function\s+(\w+)/g, '__exports.$1 = function $1')
|
|
22
|
+
|
|
23
|
+
const __exports = {}
|
|
24
|
+
// eslint-disable-next-line no-new-func
|
|
25
|
+
new Function('util', 'runtime', '__exports', stripped)(util, runtime, __exports)
|
|
26
|
+
return __exports
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Convenience: make a minimal ctx object with optional overrides. */
|
|
30
|
+
export function makeCtx(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
args: {},
|
|
33
|
+
identity: {
|
|
34
|
+
username: 'testuser',
|
|
35
|
+
sub: 'user-sub-123',
|
|
36
|
+
sourceIp: ['1.2.3.4'],
|
|
37
|
+
claims: { 'cognito:groups': [] },
|
|
38
|
+
},
|
|
39
|
+
result: null,
|
|
40
|
+
source: {},
|
|
41
|
+
error: null,
|
|
42
|
+
info: { fieldName: 'testField', parentTypeName: 'Query' },
|
|
43
|
+
stash: {},
|
|
44
|
+
...overrides,
|
|
45
|
+
}
|
|
46
|
+
}
|