@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,998 @@
|
|
|
1
|
+
/** AppSync JS resolver code for the APPSYNC_JS runtime.
|
|
2
|
+
|
|
3
|
+
Each value is a Pulumi.Input.t<string> containing a complete ES module with
|
|
4
|
+
exported `request` and `response` functions, ready to be passed as the `code`
|
|
5
|
+
field of AppSync_Resolver.makeUnitJsResolver / makePipelineJsResolver.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Shared response snippets (inlined into each resolver's template literal)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let resultResponseCode = `
|
|
13
|
+
export function response(ctx) {
|
|
14
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
15
|
+
return ctx.result;
|
|
16
|
+
}`
|
|
17
|
+
|
|
18
|
+
let firstResultResponseCode = `
|
|
19
|
+
export function response(ctx) {
|
|
20
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
21
|
+
return ctx.result.items[0] ?? null;
|
|
22
|
+
}`
|
|
23
|
+
|
|
24
|
+
let resultListResponseCode = `
|
|
25
|
+
export function response(ctx) {
|
|
26
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
27
|
+
return ctx.result.items;
|
|
28
|
+
}`
|
|
29
|
+
|
|
30
|
+
let importUtil = `import { util } from '@aws-appsync/utils';`
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Pipeline resolver pass-through (no before/after processing)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Pipeline resolver code with no pre-processing and a standard error-check response. */
|
|
37
|
+
let pipelinePassThrough =
|
|
38
|
+
`${importUtil}
|
|
39
|
+
export function request(ctx) { return {}; }
|
|
40
|
+
${resultResponseCode}
|
|
41
|
+
`->Pulumi.Input.make
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Relay Node resolver — pipeline functions for node(id: ID!) query
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/** Pipeline function (NONE datasource): decodes global ID and stashes typeName + localId. */
|
|
48
|
+
let nodeDecodeGlobalId =
|
|
49
|
+
`${importUtil}
|
|
50
|
+
export function request(ctx) {
|
|
51
|
+
const globalId = ctx.args.id;
|
|
52
|
+
try {
|
|
53
|
+
const decoded = util.base64Decode(globalId);
|
|
54
|
+
const colonIdx = decoded.indexOf(':');
|
|
55
|
+
if (colonIdx > 0) {
|
|
56
|
+
ctx.stash.typeName = decoded.substring(0, colonIdx);
|
|
57
|
+
ctx.stash.localId = decoded.substring(colonIdx + 1);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
ctx.stash.typeName = null;
|
|
61
|
+
ctx.stash.localId = null;
|
|
62
|
+
}
|
|
63
|
+
return { payload: null };
|
|
64
|
+
}
|
|
65
|
+
export function response(ctx) {
|
|
66
|
+
return ctx.stash;
|
|
67
|
+
}
|
|
68
|
+
`->Pulumi.Input.make
|
|
69
|
+
|
|
70
|
+
/** Pipeline function (DynamoDB datasource): fetches item if typeName matches, skips otherwise.
|
|
71
|
+
Generated per entity type at deploy time. */
|
|
72
|
+
let nodeGetItemForType = (~typeName: string): Pulumi.Input.t<string> =>
|
|
73
|
+
`${importUtil}
|
|
74
|
+
export function request(ctx) {
|
|
75
|
+
if (ctx.stash.typeName !== '${typeName}') return runtime.earlyReturn(ctx.prev.result);
|
|
76
|
+
return {
|
|
77
|
+
operation: 'GetItem',
|
|
78
|
+
key: { id: util.dynamodb.toDynamoDB(ctx.stash.localId) }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function response(ctx) {
|
|
82
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
83
|
+
if (!ctx.result) return null;
|
|
84
|
+
return { ...ctx.result, __typename: '${typeName}', id: ctx.args.id };
|
|
85
|
+
}
|
|
86
|
+
`->Pulumi.Input.make
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// DynamoDB read — by primary key
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
let getItemById =
|
|
93
|
+
`${importUtil}
|
|
94
|
+
export function request(ctx) {
|
|
95
|
+
return {
|
|
96
|
+
operation: 'GetItem',
|
|
97
|
+
key: { id: util.dynamodb.toDynamoDB(ctx.args.id) }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
${resultResponseCode}
|
|
101
|
+
`->Pulumi.Input.make
|
|
102
|
+
|
|
103
|
+
let queryById =
|
|
104
|
+
`${importUtil}
|
|
105
|
+
export function request(ctx) {
|
|
106
|
+
return {
|
|
107
|
+
operation: 'Query',
|
|
108
|
+
query: {
|
|
109
|
+
expression: 'id = :id',
|
|
110
|
+
expressionValues: { ':id': util.dynamodb.toDynamoDB(ctx.args.id) }
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
${resultResponseCode}
|
|
115
|
+
`->Pulumi.Input.make
|
|
116
|
+
|
|
117
|
+
/** Query by partition key with sort-key conditions and Relay cursor pagination.
|
|
118
|
+
Reads from `args.filter` object: `prefix`, `from`, `to`, `eq`, `order` (ASC/DESC).
|
|
119
|
+
Relay pagination: `first`/`after` (forward) or `last`/`before` (backward).
|
|
120
|
+
Cursor is base64 of the sort key value.
|
|
121
|
+
Returns a Relay `{ edges, pageInfo }` shape reusing the entity's `Connection` type. */
|
|
122
|
+
let queryItemsWithSortConditions = (sortField: string) =>
|
|
123
|
+
`${importUtil}
|
|
124
|
+
const encodeCursor = (skValue) => util.base64Encode(skValue);
|
|
125
|
+
const decodeCursor = (cursor) => util.base64Decode(cursor);
|
|
126
|
+
export function request(ctx) {
|
|
127
|
+
const args = ctx.args;
|
|
128
|
+
const filter = args.filter ?? {};
|
|
129
|
+
const isBackward = args.last != null;
|
|
130
|
+
const expressionNames = { '#id': 'id' };
|
|
131
|
+
const expressionValues = { ':id': util.dynamodb.toDynamoDB(args.id) };
|
|
132
|
+
let skCondition;
|
|
133
|
+
if (filter.eq != null) {
|
|
134
|
+
expressionValues[':eq'] = util.dynamodb.toDynamoDB(filter.eq);
|
|
135
|
+
skCondition = '#sk = :eq';
|
|
136
|
+
} else if (filter.prefix != null) {
|
|
137
|
+
expressionValues[':prefix'] = util.dynamodb.toDynamoDB(filter.prefix);
|
|
138
|
+
skCondition = 'begins_with(#sk, :prefix)';
|
|
139
|
+
} else if (filter.from != null && filter.to != null) {
|
|
140
|
+
expressionValues[':from'] = util.dynamodb.toDynamoDB(filter.from);
|
|
141
|
+
expressionValues[':to'] = util.dynamodb.toDynamoDB(filter.to);
|
|
142
|
+
skCondition = '#sk BETWEEN :from AND :to';
|
|
143
|
+
} else if (filter.from != null) {
|
|
144
|
+
expressionValues[':from'] = util.dynamodb.toDynamoDB(filter.from);
|
|
145
|
+
skCondition = '#sk >= :from';
|
|
146
|
+
} else if (filter.to != null) {
|
|
147
|
+
expressionValues[':to'] = util.dynamodb.toDynamoDB(filter.to);
|
|
148
|
+
skCondition = '#sk <= :to';
|
|
149
|
+
}
|
|
150
|
+
if (isBackward && args.before != null) {
|
|
151
|
+
const beforeKey = decodeCursor(args.before);
|
|
152
|
+
expressionValues[':cursor'] = util.dynamodb.toDynamoDB(beforeKey);
|
|
153
|
+
const cursorCond = '#sk < :cursor';
|
|
154
|
+
skCondition = skCondition ? \`(\${skCondition}) AND \${cursorCond}\` : cursorCond;
|
|
155
|
+
} else if (!isBackward && args.after != null) {
|
|
156
|
+
const afterKey = decodeCursor(args.after);
|
|
157
|
+
expressionValues[':cursor'] = util.dynamodb.toDynamoDB(afterKey);
|
|
158
|
+
const cursorCond = '#sk > :cursor';
|
|
159
|
+
skCondition = skCondition ? \`(\${skCondition}) AND \${cursorCond}\` : cursorCond;
|
|
160
|
+
}
|
|
161
|
+
if (skCondition) expressionNames['#sk'] = '${sortField}';
|
|
162
|
+
const expression = skCondition ? \`#id = :id AND \${skCondition}\` : '#id = :id';
|
|
163
|
+
const orderDesc = filter.order === 'DESC';
|
|
164
|
+
const scanForward = isBackward ? orderDesc : !orderDesc;
|
|
165
|
+
const pageSize = isBackward ? (args.last ?? 50) : (args.first ?? 50);
|
|
166
|
+
return {
|
|
167
|
+
operation: 'Query',
|
|
168
|
+
query: { expression, expressionNames, expressionValues },
|
|
169
|
+
scanIndexForward: scanForward,
|
|
170
|
+
limit: pageSize + 1,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function response(ctx) {
|
|
174
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
175
|
+
const args = ctx.args;
|
|
176
|
+
const filter = args.filter ?? {};
|
|
177
|
+
const isBackward = args.last != null;
|
|
178
|
+
const pageSize = isBackward ? (args.last ?? 50) : (args.first ?? 50);
|
|
179
|
+
let items = ctx.result.items ?? [];
|
|
180
|
+
const hasMore = items.length > pageSize;
|
|
181
|
+
if (hasMore) items = items.slice(0, pageSize);
|
|
182
|
+
if (isBackward) items = items.reverse();
|
|
183
|
+
const edges = items.map(item => ({ node: item, cursor: encodeCursor(item['${sortField}'] ?? '') }));
|
|
184
|
+
const startCursor = edges.length > 0 ? edges[0].cursor : null;
|
|
185
|
+
const endCursor = edges.length > 0 ? edges[edges.length - 1].cursor : null;
|
|
186
|
+
return {
|
|
187
|
+
edges,
|
|
188
|
+
pageInfo: {
|
|
189
|
+
hasNextPage: !isBackward && hasMore,
|
|
190
|
+
hasPreviousPage: isBackward && hasMore,
|
|
191
|
+
startCursor,
|
|
192
|
+
endCursor,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
`->Pulumi.Input.make
|
|
197
|
+
|
|
198
|
+
let queryByIdSort = (sortField: string) =>
|
|
199
|
+
`${importUtil}
|
|
200
|
+
export function request(ctx) {
|
|
201
|
+
return {
|
|
202
|
+
operation: 'Query',
|
|
203
|
+
query: {
|
|
204
|
+
expression: 'id = :id AND #${sortField} = :${sortField}',
|
|
205
|
+
expressionNames: { '#${sortField}': '${sortField}' },
|
|
206
|
+
expressionValues: {
|
|
207
|
+
':id': util.dynamodb.toDynamoDB(ctx.args.id),
|
|
208
|
+
':${sortField}': util.dynamodb.toDynamoDB(ctx.args.${sortField})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
${firstResultResponseCode}
|
|
214
|
+
`->Pulumi.Input.make
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// DynamoDB read — by index
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
let queryByIndex = (index: string) =>
|
|
221
|
+
`${importUtil}
|
|
222
|
+
export function request(ctx) {
|
|
223
|
+
return {
|
|
224
|
+
operation: 'Query',
|
|
225
|
+
query: {
|
|
226
|
+
expression: '#${index} = :${index}',
|
|
227
|
+
expressionNames: { '#${index}': '${index}' },
|
|
228
|
+
expressionValues: { ':${index}': util.dynamodb.toDynamoDB(ctx.args.${index}) }
|
|
229
|
+
},
|
|
230
|
+
index: '${index}'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
${resultResponseCode}
|
|
234
|
+
`->Pulumi.Input.make
|
|
235
|
+
|
|
236
|
+
let queryByIndexDeletable = (index: string) =>
|
|
237
|
+
`${importUtil}
|
|
238
|
+
export function request(ctx) {
|
|
239
|
+
const req = {
|
|
240
|
+
operation: 'Query',
|
|
241
|
+
query: {
|
|
242
|
+
expression: '#${index} = :${index}',
|
|
243
|
+
expressionNames: { '#${index}': '${index}' },
|
|
244
|
+
expressionValues: { ':${index}': util.dynamodb.toDynamoDB(ctx.args.${index}) }
|
|
245
|
+
},
|
|
246
|
+
index: '${index}'
|
|
247
|
+
};
|
|
248
|
+
if (ctx.args.hideDeleted) {
|
|
249
|
+
req.filter = {
|
|
250
|
+
expression: '#deleted = :false',
|
|
251
|
+
expressionNames: { '#deleted': 'deleted' },
|
|
252
|
+
expressionValues: { ':false': util.dynamodb.toBoolean(false) }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return req;
|
|
256
|
+
}
|
|
257
|
+
${resultResponseCode}
|
|
258
|
+
`->Pulumi.Input.make
|
|
259
|
+
|
|
260
|
+
let queryByIndexSort = (~index: string, ~idField: string, ~sortField: string) =>
|
|
261
|
+
`${importUtil}
|
|
262
|
+
export function request(ctx) {
|
|
263
|
+
const query = ctx.args.${sortField}
|
|
264
|
+
? {
|
|
265
|
+
expression: '#${idField} = :${idField} AND #${sortField} = :${sortField}',
|
|
266
|
+
expressionNames: { '#${idField}': '${idField}', '#${sortField}': '${sortField}' },
|
|
267
|
+
expressionValues: {
|
|
268
|
+
':${idField}': util.dynamodb.toDynamoDB(ctx.args.${idField}),
|
|
269
|
+
':${sortField}': util.dynamodb.toDynamoDB(ctx.args.${sortField})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
: {
|
|
273
|
+
expression: '#${idField} = :${idField}',
|
|
274
|
+
expressionNames: { '#${idField}': '${idField}' },
|
|
275
|
+
expressionValues: { ':${idField}': util.dynamodb.toDynamoDB(ctx.args.${idField}) }
|
|
276
|
+
};
|
|
277
|
+
return { operation: 'Query', query, index: '${index}' };
|
|
278
|
+
}
|
|
279
|
+
${resultResponseCode}
|
|
280
|
+
`->Pulumi.Input.make
|
|
281
|
+
|
|
282
|
+
// AppSync JS runtime restrictions (APPSYNC_JS 1.0.0):
|
|
283
|
+
// - No `for` loops (for/for-of/for-in all fail validation)
|
|
284
|
+
// - No String() / .toString() — use '' + value instead
|
|
285
|
+
// - Object.keys().forEach() works for iteration
|
|
286
|
+
let queryByIndexFiltered = (~index: string, ~idField: string) =>
|
|
287
|
+
`${importUtil}
|
|
288
|
+
export function request(ctx) {
|
|
289
|
+
const args = ctx.args;
|
|
290
|
+
const query = {
|
|
291
|
+
expression: '#${idField} = :${idField}',
|
|
292
|
+
expressionNames: { '#${idField}': '${idField}' },
|
|
293
|
+
expressionValues: { ':${idField}': util.dynamodb.toDynamoDB(args.${idField}) }
|
|
294
|
+
};
|
|
295
|
+
let expression = '';
|
|
296
|
+
const names = {};
|
|
297
|
+
const values = {};
|
|
298
|
+
Object.keys(args).forEach(key => {
|
|
299
|
+
const value = args[key];
|
|
300
|
+
if (key === '${idField}' || key === 'limit' || key === 'nextToken' || key === 'forward') return;
|
|
301
|
+
if (value == null || value === '') return;
|
|
302
|
+
if (expression) expression += ' AND';
|
|
303
|
+
if (key === 'hideDeleted') {
|
|
304
|
+
if (value === true) {
|
|
305
|
+
expression += ' #deleted = :false';
|
|
306
|
+
names['#deleted'] = 'deleted';
|
|
307
|
+
values[':false'] = false;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
expression += ' contains(#' + key + ', :' + key + ')';
|
|
311
|
+
names['#' + key] = key;
|
|
312
|
+
values[':' + key] = '' + value;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const result = {
|
|
316
|
+
operation: 'Query',
|
|
317
|
+
query,
|
|
318
|
+
index: '${index}',
|
|
319
|
+
limit: (args.limit ?? 50),
|
|
320
|
+
nextToken: (args.nextToken ?? null),
|
|
321
|
+
scanIndexForward: (args.forward ?? true)
|
|
322
|
+
};
|
|
323
|
+
if (expression) {
|
|
324
|
+
result.filter = { expression, expressionNames: names, expressionValues: util.dynamodb.toMapValues(values) };
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
${resultResponseCode}
|
|
329
|
+
`->Pulumi.Input.make
|
|
330
|
+
|
|
331
|
+
let queryByIndexSortFiltered = (~index: string, ~idField: string, ~sortField: string) =>
|
|
332
|
+
`${importUtil}
|
|
333
|
+
export function request(ctx) {
|
|
334
|
+
const args = ctx.args;
|
|
335
|
+
const query = args.${sortField}
|
|
336
|
+
? {
|
|
337
|
+
expression: '#${idField} = :${idField} AND #${sortField} = :${sortField}',
|
|
338
|
+
expressionNames: { '#${idField}': '${idField}', '#${sortField}': '${sortField}' },
|
|
339
|
+
expressionValues: {
|
|
340
|
+
':${idField}': util.dynamodb.toDynamoDB(args.${idField}),
|
|
341
|
+
':${sortField}': util.dynamodb.toDynamoDB(args.${sortField})
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
: {
|
|
345
|
+
expression: '#${idField} = :${idField}',
|
|
346
|
+
expressionNames: { '#${idField}': '${idField}' },
|
|
347
|
+
expressionValues: { ':${idField}': util.dynamodb.toDynamoDB(args.${idField}) }
|
|
348
|
+
};
|
|
349
|
+
let expression = '';
|
|
350
|
+
const names = {};
|
|
351
|
+
const values = {};
|
|
352
|
+
Object.keys(args).forEach(key => {
|
|
353
|
+
const value = args[key];
|
|
354
|
+
if (key === '${idField}' || key === '${sortField}' || key === 'limit' || key === 'nextToken' || key === 'forward') return;
|
|
355
|
+
if (value == null || value === '') return;
|
|
356
|
+
if (expression) expression += ' AND';
|
|
357
|
+
if (key === 'hideDeleted') {
|
|
358
|
+
if (value === true) {
|
|
359
|
+
expression += ' #deleted = :false';
|
|
360
|
+
names['#deleted'] = 'deleted';
|
|
361
|
+
values[':false'] = false;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
expression += ' contains(#' + key + ', :' + key + ')';
|
|
365
|
+
names['#' + key] = key;
|
|
366
|
+
values[':' + key] = '' + value;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
const result = {
|
|
370
|
+
operation: 'Query',
|
|
371
|
+
query,
|
|
372
|
+
index: '${index}',
|
|
373
|
+
limit: (args.limit ?? 50),
|
|
374
|
+
nextToken: (args.nextToken ?? null),
|
|
375
|
+
scanIndexForward: (args.forward ?? true)
|
|
376
|
+
};
|
|
377
|
+
if (expression) {
|
|
378
|
+
result.filter = { expression, expressionNames: names, expressionValues: util.dynamodb.toMapValues(values) };
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
${resultResponseCode}
|
|
383
|
+
`->Pulumi.Input.make
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// DynamoDB read — list all
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
let listAllItems =
|
|
390
|
+
`${importUtil}
|
|
391
|
+
export function request(ctx) {
|
|
392
|
+
return {
|
|
393
|
+
operation: 'Scan',
|
|
394
|
+
limit: (ctx.args.limit ?? 50),
|
|
395
|
+
nextToken: (ctx.args.nextToken ?? null)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
${resultResponseCode}
|
|
399
|
+
`->Pulumi.Input.make
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// DynamoDB read — list all (Relay Connection spec)
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Scan with optional `filter` arg: `{search?, searchPrefix?, ids?, <field>Eq?, <field>From?, <field>To?}`
|
|
407
|
+
* and optional `orderBy: {field, direction}`.
|
|
408
|
+
*
|
|
409
|
+
* `search` → `contains(labelField, :v)` — case-sensitive on DynamoDB. Callers that
|
|
410
|
+
* need case-insensitive matching should project a lowercased label column
|
|
411
|
+
* (future Phase 6.1 / external full-text search).
|
|
412
|
+
* `searchPrefix` → `begins_with(labelField, :v)` — case-sensitive. Scan-only here;
|
|
413
|
+
* Phase 6 `@searchable` provisions a GSI to promote this to a query.
|
|
414
|
+
* `ids` → FilterExpression `#id IN (:id0, :id1, …)`. Simple scan-based
|
|
415
|
+
* path; BatchGetItem optimisation is deferred (open question 1).
|
|
416
|
+
* `<field>Eq` → FilterExpression `#<field> = :<field>Eq`, one per `~filterFields` entry.
|
|
417
|
+
* `<field>From` → FilterExpression `#<field> >= :<field>From`, one per `~rangeFields` entry.
|
|
418
|
+
* `<field>To` → FilterExpression `#<field> <= :<field>To`, one per `~rangeFields` entry.
|
|
419
|
+
* `orderBy` → JS-runtime sort over the returned page when `orderBy.field` is in
|
|
420
|
+
* `~sortFields`. **Per-page only**, not global — DynamoDB Scan returns
|
|
421
|
+
* items in indeterminate order and `ScanIndexForward` does not apply
|
|
422
|
+
* to Scan. Index-routed Query (v1.5) lifts this caveat for indexed
|
|
423
|
+
* sort fields; `@scanSort` on a non-indexed field is per-page even then.
|
|
424
|
+
*
|
|
425
|
+
* Empty-string and null filter values are treated as "no filter" — consistent with the
|
|
426
|
+
* in-memory adapter so clients don't need to conditionally omit keys.
|
|
427
|
+
*/
|
|
428
|
+
let listAllItemsConnection = (
|
|
429
|
+
~labelField: string,
|
|
430
|
+
~filterFields: array<string>=[],
|
|
431
|
+
~rangeFields: array<string>=[],
|
|
432
|
+
~sortFields: array<string>=[],
|
|
433
|
+
) => {
|
|
434
|
+
let filterClauses =
|
|
435
|
+
filterFields
|
|
436
|
+
->Array.map(f => `
|
|
437
|
+
if (filter.${f}Eq !== undefined && filter.${f}Eq !== null && filter.${f}Eq !== '') {
|
|
438
|
+
names['#${f}'] = '${f}';
|
|
439
|
+
values[':${f}Eq'] = util.dynamodb.toDynamoDB(filter.${f}Eq);
|
|
440
|
+
parts.push('#${f} = :${f}Eq');
|
|
441
|
+
}`)
|
|
442
|
+
->Array.join("")
|
|
443
|
+
let rangeClauses =
|
|
444
|
+
rangeFields
|
|
445
|
+
->Array.map(f => `
|
|
446
|
+
if (filter.${f}From !== undefined && filter.${f}From !== null && filter.${f}From !== '') {
|
|
447
|
+
names['#${f}'] = '${f}';
|
|
448
|
+
values[':${f}From'] = util.dynamodb.toDynamoDB(filter.${f}From);
|
|
449
|
+
parts.push('#${f} >= :${f}From');
|
|
450
|
+
}
|
|
451
|
+
if (filter.${f}To !== undefined && filter.${f}To !== null && filter.${f}To !== '') {
|
|
452
|
+
names['#${f}'] = '${f}';
|
|
453
|
+
values[':${f}To'] = util.dynamodb.toDynamoDB(filter.${f}To);
|
|
454
|
+
parts.push('#${f} <= :${f}To');
|
|
455
|
+
}`)
|
|
456
|
+
->Array.join("")
|
|
457
|
+
let sortFieldsLiteral =
|
|
458
|
+
sortFields->Array.map(f => `'${f}'`)->Array.join(", ")
|
|
459
|
+
let sortBlock = if sortFields->Array.length == 0 {
|
|
460
|
+
""
|
|
461
|
+
} else {
|
|
462
|
+
// APPSYNC_JS 1.0.0 forbids: Array.prototype.sort(comparator), arrow/function
|
|
463
|
+
// expressions passed to sort, for/while loops, recursion, and ++/--. So we
|
|
464
|
+
// can't run a comparator-driven sort and we can't write our own loop. Use a
|
|
465
|
+
// schwartzian transform: encode each item as `<sortKey>\x01<json>`, run the
|
|
466
|
+
// no-comparator default sort (lexicographic), reverse for DESC, and decode.
|
|
467
|
+
// Numeric fields get zero-padded so lex order matches numeric order for
|
|
468
|
+
// non-negative values (typical for IDs, counts, timestamps). Negatives sort
|
|
469
|
+
// lexicographically — acceptable since DynamoDB sort keys are rarely signed
|
|
470
|
+
// numbers. Nulls split out and append to the end regardless of direction.
|
|
471
|
+
`
|
|
472
|
+
// Per-page sort (Scan returns items in indeterminate order; ScanIndexForward
|
|
473
|
+
// does not apply to Scan). Global ordering across pages requires v1.5 index
|
|
474
|
+
// promotion; @scanSort is per-page even then.
|
|
475
|
+
const orderBy = ctx.args.orderBy;
|
|
476
|
+
const sortFields = [${sortFieldsLiteral}];
|
|
477
|
+
if (orderBy && orderBy.field && sortFields.indexOf(orderBy.field) >= 0) {
|
|
478
|
+
const field = orderBy.field;
|
|
479
|
+
const nulls = items.filter(it => it[field] === null || it[field] === undefined);
|
|
480
|
+
const nonNulls = items.filter(it => it[field] !== null && it[field] !== undefined);
|
|
481
|
+
const encoded = nonNulls.map(it => {
|
|
482
|
+
const v = it[field];
|
|
483
|
+
const key = (typeof v === 'number')
|
|
484
|
+
? ('0000000000000000000000' + v).slice(-22)
|
|
485
|
+
: ('' + v);
|
|
486
|
+
return key + '\\x01' + JSON.stringify(it);
|
|
487
|
+
});
|
|
488
|
+
encoded.sort();
|
|
489
|
+
if (orderBy.direction === 'DESC') encoded.reverse();
|
|
490
|
+
items = encoded.map(e => JSON.parse(e.split('\\x01')[1])).concat(nulls);
|
|
491
|
+
}`
|
|
492
|
+
}
|
|
493
|
+
`${importUtil}
|
|
494
|
+
export function request(ctx) {
|
|
495
|
+
const filter = ctx.args.filter ?? {};
|
|
496
|
+
const names = {};
|
|
497
|
+
const values = {};
|
|
498
|
+
const parts = [];
|
|
499
|
+
if (typeof filter.search === 'string' && filter.search.length > 0) {
|
|
500
|
+
names['#label'] = '${labelField}';
|
|
501
|
+
values[':search'] = util.dynamodb.toDynamoDB(filter.search);
|
|
502
|
+
parts.push('contains(#label, :search)');
|
|
503
|
+
}
|
|
504
|
+
if (typeof filter.searchPrefix === 'string' && filter.searchPrefix.length > 0) {
|
|
505
|
+
names['#label'] = '${labelField}';
|
|
506
|
+
values[':searchPrefix'] = util.dynamodb.toDynamoDB(filter.searchPrefix);
|
|
507
|
+
parts.push('begins_with(#label, :searchPrefix)');
|
|
508
|
+
}
|
|
509
|
+
if (Array.isArray(filter.ids) && filter.ids.length > 0) {
|
|
510
|
+
names['#id'] = 'id';
|
|
511
|
+
const placeholders = filter.ids.map((id, i) => {
|
|
512
|
+
const key = ':id' + i;
|
|
513
|
+
values[key] = util.dynamodb.toDynamoDB(id);
|
|
514
|
+
return key;
|
|
515
|
+
});
|
|
516
|
+
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
517
|
+
}${filterClauses}${rangeClauses}
|
|
518
|
+
const req = {
|
|
519
|
+
operation: 'Scan',
|
|
520
|
+
limit: (ctx.args.first ?? 50),
|
|
521
|
+
nextToken: (ctx.args.after ?? null),
|
|
522
|
+
};
|
|
523
|
+
if (parts.length > 0) {
|
|
524
|
+
req.filter = {
|
|
525
|
+
expression: parts.join(' AND '),
|
|
526
|
+
expressionNames: names,
|
|
527
|
+
expressionValues: values,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return req;
|
|
531
|
+
}
|
|
532
|
+
export function response(ctx) {
|
|
533
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
534
|
+
let items = ctx.result?.items ?? [];${sortBlock}
|
|
535
|
+
const edges = items.map((item, i) => ({
|
|
536
|
+
node: item,
|
|
537
|
+
cursor: ctx.args.after ? ctx.args.after + '_' + i : '' + i,
|
|
538
|
+
}));
|
|
539
|
+
return {
|
|
540
|
+
edges,
|
|
541
|
+
pageInfo: {
|
|
542
|
+
hasNextPage: !!ctx.result?.nextToken,
|
|
543
|
+
hasPreviousPage: !!ctx.args.after,
|
|
544
|
+
startCursor: edges.length > 0 ? edges[0].cursor : null,
|
|
545
|
+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
`->Pulumi.Input.make
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// DynamoDB nested resolvers — resolve linked item(s) by ID
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
let resolveId = (~sourceIdField: string) =>
|
|
557
|
+
`${importUtil}
|
|
558
|
+
export function request(ctx) {
|
|
559
|
+
if (!ctx.source.${sourceIdField}) return runtime.earlyReturn(null);
|
|
560
|
+
return {
|
|
561
|
+
operation: 'Query',
|
|
562
|
+
query: {
|
|
563
|
+
expression: '#id = :id',
|
|
564
|
+
expressionNames: { '#id': 'id' },
|
|
565
|
+
expressionValues: { ':id': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}) }
|
|
566
|
+
},
|
|
567
|
+
limit: (ctx.args.limit ?? 50),
|
|
568
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
569
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
${firstResultResponseCode}
|
|
573
|
+
`->Pulumi.Input.make
|
|
574
|
+
|
|
575
|
+
let resolveIdSort = (~sourceIdField: string, ~sourceSortField: string, ~targetSortField: string) =>
|
|
576
|
+
`${importUtil}
|
|
577
|
+
export function request(ctx) {
|
|
578
|
+
return {
|
|
579
|
+
operation: 'Query',
|
|
580
|
+
query: {
|
|
581
|
+
expression: '#id = :id AND #${targetSortField} = :${targetSortField}',
|
|
582
|
+
expressionNames: { '#id': 'id', '#${targetSortField}': '${targetSortField}' },
|
|
583
|
+
expressionValues: {
|
|
584
|
+
':id': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}),
|
|
585
|
+
':${targetSortField}': util.dynamodb.toDynamoDB(ctx.source.${sourceSortField})
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
limit: (ctx.args.limit ?? 50),
|
|
589
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
590
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
${firstResultResponseCode}
|
|
594
|
+
`->Pulumi.Input.make
|
|
595
|
+
|
|
596
|
+
let resolveIdSortArgument = (
|
|
597
|
+
~sourceIdField: string,
|
|
598
|
+
~sourceSortArgument: string,
|
|
599
|
+
~targetSortField: string,
|
|
600
|
+
) =>
|
|
601
|
+
`${importUtil}
|
|
602
|
+
export function request(ctx) {
|
|
603
|
+
const query = ctx.args.${sourceSortArgument}
|
|
604
|
+
? {
|
|
605
|
+
expression: '#id = :id AND #${targetSortField} = :${targetSortField}',
|
|
606
|
+
expressionNames: { '#id': 'id', '#${targetSortField}': '${targetSortField}' },
|
|
607
|
+
expressionValues: {
|
|
608
|
+
':id': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}),
|
|
609
|
+
':${targetSortField}': util.dynamodb.toDynamoDB(ctx.args.${sourceSortArgument})
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
: {
|
|
613
|
+
expression: '#id = :id',
|
|
614
|
+
expressionNames: { '#id': 'id' },
|
|
615
|
+
expressionValues: { ':id': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}) }
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
operation: 'Query',
|
|
619
|
+
query,
|
|
620
|
+
limit: (ctx.args.limit ?? 50),
|
|
621
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
622
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
${firstResultResponseCode}
|
|
626
|
+
`->Pulumi.Input.make
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// DynamoDB nested resolvers — resolve by index
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
let resolveIdByIndex = (~index: string, ~sourceIdField: string, ~targetIdField: string) =>
|
|
633
|
+
`${importUtil}
|
|
634
|
+
export function request(ctx) {
|
|
635
|
+
return {
|
|
636
|
+
operation: 'Query',
|
|
637
|
+
query: {
|
|
638
|
+
expression: '#${targetIdField} = :${targetIdField}',
|
|
639
|
+
expressionNames: { '#${targetIdField}': '${targetIdField}' },
|
|
640
|
+
expressionValues: { ':${targetIdField}': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}) }
|
|
641
|
+
},
|
|
642
|
+
index: '${index}',
|
|
643
|
+
limit: (ctx.args.limit ?? 50),
|
|
644
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
645
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
${firstResultResponseCode}
|
|
649
|
+
`->Pulumi.Input.make
|
|
650
|
+
|
|
651
|
+
let resolveIdByIndexSort = (
|
|
652
|
+
~index: string,
|
|
653
|
+
~sourceIdField: string,
|
|
654
|
+
~sourceSortField: string,
|
|
655
|
+
~targetIdField: string,
|
|
656
|
+
~targetSortField: string,
|
|
657
|
+
) =>
|
|
658
|
+
`${importUtil}
|
|
659
|
+
export function request(ctx) {
|
|
660
|
+
return {
|
|
661
|
+
operation: 'Query',
|
|
662
|
+
query: {
|
|
663
|
+
expression: '#${targetIdField} = :${targetIdField} AND #${targetSortField} = :${targetSortField}',
|
|
664
|
+
expressionNames: { '#${targetIdField}': '${targetIdField}', '#${targetSortField}': '${targetSortField}' },
|
|
665
|
+
expressionValues: {
|
|
666
|
+
':${targetIdField}': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}),
|
|
667
|
+
':${targetSortField}': util.dynamodb.toDynamoDB(ctx.source.${sourceSortField})
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
index: '${index}',
|
|
671
|
+
limit: (ctx.args.limit ?? 50),
|
|
672
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
673
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
${firstResultResponseCode}
|
|
677
|
+
`->Pulumi.Input.make
|
|
678
|
+
|
|
679
|
+
let resolveIdByIndexSortArgument = (
|
|
680
|
+
~index: string,
|
|
681
|
+
~sourceIdField: string,
|
|
682
|
+
~sourceSortArgument: string,
|
|
683
|
+
~targetIdField: string,
|
|
684
|
+
~targetSortField: string,
|
|
685
|
+
) =>
|
|
686
|
+
`${importUtil}
|
|
687
|
+
export function request(ctx) {
|
|
688
|
+
const query = ctx.args.${sourceSortArgument}
|
|
689
|
+
? {
|
|
690
|
+
expression: '#${targetIdField} = :${targetIdField} AND #${targetSortField} = :${targetSortField}',
|
|
691
|
+
expressionNames: { '#${targetIdField}': '${targetIdField}', '#${targetSortField}': '${targetSortField}' },
|
|
692
|
+
expressionValues: {
|
|
693
|
+
':${targetIdField}': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}),
|
|
694
|
+
':${targetSortField}': util.dynamodb.toDynamoDB(ctx.args.${sourceSortArgument})
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
: {
|
|
698
|
+
expression: '#${targetIdField} = :${targetIdField}',
|
|
699
|
+
expressionNames: { '#${targetIdField}': '${targetIdField}' },
|
|
700
|
+
expressionValues: { ':${targetIdField}': util.dynamodb.toDynamoDB(ctx.source.${sourceIdField}) }
|
|
701
|
+
};
|
|
702
|
+
return {
|
|
703
|
+
operation: 'Query',
|
|
704
|
+
query,
|
|
705
|
+
index: '${index}',
|
|
706
|
+
limit: (ctx.args.limit ?? 50),
|
|
707
|
+
nextToken: (ctx.args.nextToken ?? null),
|
|
708
|
+
scanIndexForward: (ctx.args.forward ?? true)
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
${firstResultResponseCode}
|
|
712
|
+
`->Pulumi.Input.make
|
|
713
|
+
|
|
714
|
+
// resolveIds — returns a plain string (table name is interpolated by the adapter
|
|
715
|
+
// via Pulumi.Output.apply)
|
|
716
|
+
let resolveIds = (tableName: string, ~idsField: string, ~sortField: option<string>) => {
|
|
717
|
+
let keysCode = switch sortField {
|
|
718
|
+
| Some(sf) =>
|
|
719
|
+
`id => ({ id: util.dynamodb.toString(id.id), ${sf}: util.dynamodb.toString(id.${sf}) })`
|
|
720
|
+
| None => `id => ({ id: util.dynamodb.toString(id) })`
|
|
721
|
+
}
|
|
722
|
+
`${importUtil}
|
|
723
|
+
import { runtime } from '@aws-appsync/utils';
|
|
724
|
+
export function request(ctx) {
|
|
725
|
+
const idList = ctx.source.${idsField};
|
|
726
|
+
if (idList && idList.length > 0) {
|
|
727
|
+
return {
|
|
728
|
+
operation: 'BatchGetItem',
|
|
729
|
+
tables: {
|
|
730
|
+
'${tableName}': {
|
|
731
|
+
keys: idList.map(${keysCode}),
|
|
732
|
+
consistentRead: true
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return { operation: 'GetItem', key: { id: util.dynamodb.toString(ctx.source.id) } };
|
|
738
|
+
}
|
|
739
|
+
export function response(ctx) {
|
|
740
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
741
|
+
return ctx.result;
|
|
742
|
+
}
|
|
743
|
+
`
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Batched-by-ids — top-level Query resolver reading `ctx.args.ids: [String!]!`
|
|
747
|
+
and returning the matching items via a single BatchGetItem. Missing ids drop
|
|
748
|
+
out (BatchGetItem does not preserve cardinality); empty input short-circuits
|
|
749
|
+
to an empty result without hitting DDB. Table name is interpolated at deploy
|
|
750
|
+
time, since BatchGetItem's `tables` map keys on the literal table name.
|
|
751
|
+
Single-key tables only — composite-key BatchGetItem needs both pk + sk per
|
|
752
|
+
key entry, which this template doesn't construct. */
|
|
753
|
+
let batchGetItemsByIds = (tableName: string) =>
|
|
754
|
+
`${importUtil}
|
|
755
|
+
import { runtime } from '@aws-appsync/utils';
|
|
756
|
+
export function request(ctx) {
|
|
757
|
+
const ids = ctx.args.ids ?? [];
|
|
758
|
+
if (ids.length === 0) return runtime.earlyReturn([]);
|
|
759
|
+
return {
|
|
760
|
+
operation: 'BatchGetItem',
|
|
761
|
+
tables: {
|
|
762
|
+
'${tableName}': {
|
|
763
|
+
keys: ids.map(id => ({ id: util.dynamodb.toDynamoDB(id) })),
|
|
764
|
+
consistentRead: true,
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
export function response(ctx) {
|
|
770
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
771
|
+
// BatchGetItem returns null in the result array for keys that don't exist
|
|
772
|
+
// in the table, preserving index correspondence with the input. The SDL
|
|
773
|
+
// returns this field as \`[T!]!\` (non-null element list), so any single
|
|
774
|
+
// missing id makes the entire field fail with "Cannot return null for
|
|
775
|
+
// non-nullable type" and the caller sees data=null. Filter the nulls so
|
|
776
|
+
// the field returns just the items that were found.
|
|
777
|
+
return (ctx.result?.data?.['${tableName}'] ?? []).filter(item => item !== null);
|
|
778
|
+
}
|
|
779
|
+
`
|
|
780
|
+
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// DynamoDB write
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
let putItem =
|
|
786
|
+
`${importUtil}
|
|
787
|
+
export function request(ctx) {
|
|
788
|
+
return {
|
|
789
|
+
operation: 'PutItem',
|
|
790
|
+
key: { id: util.dynamodb.toDynamoDB(ctx.args.id) },
|
|
791
|
+
attributeValues: util.dynamodb.toMapValues(ctx.args)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
${resultResponseCode}
|
|
795
|
+
`->Pulumi.Input.make
|
|
796
|
+
|
|
797
|
+
let addItemToList = (~listName: string, ~itemName: string) =>
|
|
798
|
+
`${importUtil}
|
|
799
|
+
export function request(ctx) {
|
|
800
|
+
return {
|
|
801
|
+
operation: 'UpdateItem',
|
|
802
|
+
key: { id: util.dynamodb.toDynamoDB(ctx.args.id) },
|
|
803
|
+
update: {
|
|
804
|
+
expression: 'SET #list = list_append(#list, :item)',
|
|
805
|
+
expressionNames: { '#list': '${listName}' },
|
|
806
|
+
expressionValues: { ':item': util.dynamodb.toList([util.dynamodb.toDynamoDB(ctx.args.${itemName})]) }
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
${resultResponseCode}
|
|
811
|
+
`->Pulumi.Input.make
|
|
812
|
+
|
|
813
|
+
let deleteItem =
|
|
814
|
+
`${importUtil}
|
|
815
|
+
export function request(ctx) {
|
|
816
|
+
return {
|
|
817
|
+
operation: 'DeleteItem',
|
|
818
|
+
key: { id: util.dynamodb.toString(ctx.args.id) }
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
${resultResponseCode}
|
|
822
|
+
`->Pulumi.Input.make
|
|
823
|
+
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
// Lambda invocations
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
/** Invoke a CommandGenerator Lambda. `command` is the Aggregate command name. */
|
|
829
|
+
let invokeCommandGenerator = (command: string) =>
|
|
830
|
+
`${importUtil}
|
|
831
|
+
export function request(ctx) {
|
|
832
|
+
const id = ctx.identity;
|
|
833
|
+
const isCognito = id != null && id.sub != null;
|
|
834
|
+
return {
|
|
835
|
+
operation: 'Invoke',
|
|
836
|
+
payload: {
|
|
837
|
+
command: '${command}',
|
|
838
|
+
arguments: ctx.args,
|
|
839
|
+
meta: {
|
|
840
|
+
ip: id?.sourceIp ?? null,
|
|
841
|
+
user: id?.username ?? null,
|
|
842
|
+
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
843
|
+
},
|
|
844
|
+
identity: isCognito
|
|
845
|
+
? {
|
|
846
|
+
userId: id.sub,
|
|
847
|
+
username: id.username,
|
|
848
|
+
groups: id.claims?.['cognito:groups'] ?? [],
|
|
849
|
+
claims: id.claims,
|
|
850
|
+
provider: 'Cognito'
|
|
851
|
+
}
|
|
852
|
+
: id != null
|
|
853
|
+
? {
|
|
854
|
+
userArn: id.userArn ?? null,
|
|
855
|
+
accountId: id.accountId ?? null,
|
|
856
|
+
username: id.username ?? null,
|
|
857
|
+
provider: 'IAM'
|
|
858
|
+
}
|
|
859
|
+
: null
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
${resultResponseCode}
|
|
864
|
+
`->Pulumi.Input.make
|
|
865
|
+
|
|
866
|
+
/** Invoke a DCB Lambda. `tag` is the DCB entity tag. */
|
|
867
|
+
let invokeDcbMutation = (tag: string) =>
|
|
868
|
+
`${importUtil}
|
|
869
|
+
export function request(ctx) {
|
|
870
|
+
const id = ctx.identity;
|
|
871
|
+
const isCognito = id != null && id.sub != null;
|
|
872
|
+
return {
|
|
873
|
+
operation: 'Invoke',
|
|
874
|
+
payload: {
|
|
875
|
+
command: '${tag}',
|
|
876
|
+
arguments: ctx.args,
|
|
877
|
+
meta: {
|
|
878
|
+
ip: id?.sourceIp ?? null,
|
|
879
|
+
user: id?.username ?? null,
|
|
880
|
+
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
881
|
+
},
|
|
882
|
+
identity: isCognito
|
|
883
|
+
? {
|
|
884
|
+
userId: id.sub,
|
|
885
|
+
username: id.username,
|
|
886
|
+
groups: id.claims?.['cognito:groups'] ?? [],
|
|
887
|
+
claims: id.claims,
|
|
888
|
+
provider: 'Cognito'
|
|
889
|
+
}
|
|
890
|
+
: id != null
|
|
891
|
+
? {
|
|
892
|
+
userArn: id.userArn ?? null,
|
|
893
|
+
accountId: id.accountId ?? null,
|
|
894
|
+
username: id.username ?? null,
|
|
895
|
+
provider: 'IAM'
|
|
896
|
+
}
|
|
897
|
+
: null
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
${resultResponseCode}
|
|
902
|
+
`->Pulumi.Input.make
|
|
903
|
+
|
|
904
|
+
/** Invoke an InboundTranslation Lambda. `fieldName` is the mutation field. */
|
|
905
|
+
let invokeInboundTranslation = (fieldName: string) =>
|
|
906
|
+
`${importUtil}
|
|
907
|
+
export function request(ctx) {
|
|
908
|
+
return {
|
|
909
|
+
operation: 'Invoke',
|
|
910
|
+
payload: {
|
|
911
|
+
__inboundTranslation: true,
|
|
912
|
+
fieldName: '${fieldName}',
|
|
913
|
+
arguments: ctx.args
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
${resultResponseCode}
|
|
918
|
+
`->Pulumi.Input.make
|
|
919
|
+
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
// Authorization (pipeline function fragments)
|
|
922
|
+
// ---------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
let uncapitalize = str =>
|
|
925
|
+
switch String.get(str, 0) {
|
|
926
|
+
| None => ""
|
|
927
|
+
| Some(first) => String.concat(first->String.toLowerCase, String.slice(str, ~start=1))
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** Combined pipeline function: checks group membership, fetches the item, and
|
|
931
|
+
verifies the item's owner matches the requesting user.
|
|
932
|
+
Replaces the two separate VTL authorizeIndexedAccessRequest/Response templates,
|
|
933
|
+
which were the request and response halves of the same AppSync Function. */
|
|
934
|
+
let authorizeIndexedAccess = (~index: string, ~group: string) => {
|
|
935
|
+
let authIdName = group->uncapitalize ++ "Id"
|
|
936
|
+
`${importUtil}
|
|
937
|
+
import { runtime } from '@aws-appsync/utils';
|
|
938
|
+
export function request(ctx) {
|
|
939
|
+
const groups = ctx.identity.claims?.['cognito:groups'] ?? [];
|
|
940
|
+
if (groups.includes('${group}')) {
|
|
941
|
+
return { operation: 'GetItem', key: { id: util.dynamodb.toDynamoDB(ctx.args.${index}) } };
|
|
942
|
+
}
|
|
943
|
+
return runtime.earlyReturn({ ${authIdName}: [ctx.identity.username] });
|
|
944
|
+
}
|
|
945
|
+
export function response(ctx) {
|
|
946
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
947
|
+
if (ctx.result.${authIdName} === ctx.identity.username) return ctx.result;
|
|
948
|
+
return util.unauthorized();
|
|
949
|
+
}
|
|
950
|
+
`->Pulumi.Input.make
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** @deprecated Use authorizeIndexedAccess — kept for naming continuity with VTL templates. */
|
|
954
|
+
let authorizeIndexedAccessRequest = authorizeIndexedAccess
|
|
955
|
+
|
|
956
|
+
/** @deprecated The response check is now part of authorizeIndexedAccess. */
|
|
957
|
+
let authorizeIndexedAccessResponse = (~group as _: string) =>
|
|
958
|
+
resultResponseCode->Pulumi.Input.make
|
|
959
|
+
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// Response-only helpers
|
|
962
|
+
// (kept for adapter compatibility; these are now embedded in each resolver above)
|
|
963
|
+
// ---------------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
let result = resultResponseCode->Pulumi.Input.make
|
|
966
|
+
let firstResult = firstResultResponseCode->Pulumi.Input.make
|
|
967
|
+
let resultList = resultListResponseCode->Pulumi.Input.make
|
|
968
|
+
|
|
969
|
+
let resolveIdResult = (tableName: string, ~idField: string) =>
|
|
970
|
+
`
|
|
971
|
+
export function response(ctx) {
|
|
972
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
973
|
+
if (ctx.source.${idField}) return ctx.result.data['${tableName}'][0] ?? null;
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
`->Pulumi.Input.make
|
|
977
|
+
|
|
978
|
+
let resolveIdResults = (tableName: string, ~idField: string) =>
|
|
979
|
+
`
|
|
980
|
+
export function response(ctx) {
|
|
981
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
982
|
+
if (ctx.source.${idField}) return ctx.result.data['${tableName}'] ?? null;
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
`
|
|
986
|
+
|
|
987
|
+
let resolveIdsResult = (tableName: string, ~idsField: string) =>
|
|
988
|
+
`
|
|
989
|
+
export function response(ctx) {
|
|
990
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
991
|
+
const idList = ctx.source.${idsField};
|
|
992
|
+
return (idList && idList.length > 0) ? ctx.result.data['${tableName}'] : [];
|
|
993
|
+
}
|
|
994
|
+
`
|
|
995
|
+
|
|
996
|
+
let null = `
|
|
997
|
+
export function response(ctx) { return null; }
|
|
998
|
+
`->Pulumi.Input.make
|