1
- import { ChangeHotswapResult , classifyChanges , HotswappableChangeCandidate , lowerCaseFirstCharacter , reportNonHotswappableChange , transformObjectKeys } from './common' ;
1
+ import { GetSchemaCreationStatusRequest , GetSchemaCreationStatusResponse } from 'aws-sdk/clients/appsync' ;
2
+ import { ChangeHotswapResult , classifyChanges , HotswappableChangeCandidate , lowerCaseFirstCharacter , transformObjectKeys } from './common' ;
2
3
import { ISDK } from '../aws-auth' ;
4
+
3
5
import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template' ;
4
6
5
7
export async function isHotswappableAppSyncChange (
6
8
logicalId : string , change : HotswappableChangeCandidate , evaluateCfnTemplate : EvaluateCloudFormationTemplate ,
7
9
) : Promise < ChangeHotswapResult > {
8
10
const isResolver = change . newValue . Type === 'AWS::AppSync::Resolver' ;
9
11
const isFunction = change . newValue . Type === 'AWS::AppSync::FunctionConfiguration' ;
12
+ const isGraphQLSchema = change . newValue . Type === 'AWS::AppSync::GraphQLSchema' ;
10
13
11
- if ( ! isResolver && ! isFunction ) {
14
+ if ( ! isResolver && ! isFunction && ! isGraphQLSchema ) {
12
15
return [ ] ;
13
16
}
14
17
15
18
const ret : ChangeHotswapResult = [ ] ;
16
- if ( isResolver && change . newValue . Properties ?. Kind === 'PIPELINE' ) {
17
- reportNonHotswappableChange (
18
- ret ,
19
- change ,
20
- undefined ,
21
- 'Pipeline resolvers cannot be hotswapped since they reference the FunctionId of the underlying functions, which cannot be resolved' ,
22
- ) ;
23
- return ret ;
24
- }
25
19
26
- const classifiedChanges = classifyChanges ( change , [ 'RequestMappingTemplate' , 'ResponseMappingTemplate' ] ) ;
20
+ const classifiedChanges = classifyChanges ( change , [
21
+ 'RequestMappingTemplate' ,
22
+ 'RequestMappingTemplateS3Location' ,
23
+ 'ResponseMappingTemplate' ,
24
+ 'ResponseMappingTemplateS3Location' ,
25
+ 'Code' ,
26
+ 'CodeS3Location' ,
27
+ 'Definition' ,
28
+ 'DefinitionS3Location' ,
29
+ ] ) ;
27
30
classifiedChanges . reportNonHotswappablePropertyChanges ( ret ) ;
28
31
29
32
const namesOfHotswappableChanges = Object . keys ( classifiedChanges . hotswappableProps ) ;
@@ -49,25 +52,86 @@ export async function isHotswappableAppSyncChange(
49
52
50
53
const sdkProperties : { [ name : string ] : any } = {
51
54
...change . oldValue . Properties ,
55
+ Definition : change . newValue . Properties ?. Definition ,
56
+ DefinitionS3Location : change . newValue . Properties ?. DefinitionS3Location ,
52
57
requestMappingTemplate : change . newValue . Properties ?. RequestMappingTemplate ,
58
+ requestMappingTemplateS3Location : change . newValue . Properties ?. RequestMappingTemplateS3Location ,
53
59
responseMappingTemplate : change . newValue . Properties ?. ResponseMappingTemplate ,
60
+ responseMappingTemplateS3Location : change . newValue . Properties ?. ResponseMappingTemplateS3Location ,
61
+ code : change . newValue . Properties ?. Code ,
62
+ codeS3Location : change . newValue . Properties ?. CodeS3Location ,
54
63
} ;
55
64
const evaluatedResourceProperties = await evaluateCfnTemplate . evaluateCfnExpression ( sdkProperties ) ;
56
65
const sdkRequestObject = transformObjectKeys ( evaluatedResourceProperties , lowerCaseFirstCharacter ) ;
57
66
67
+ // resolve s3 location files as SDK doesn't take in s3 location but inline code
68
+ if ( sdkRequestObject . requestMappingTemplateS3Location ) {
69
+ sdkRequestObject . requestMappingTemplate = ( await fetchFileFromS3 ( sdkRequestObject . requestMappingTemplateS3Location , sdk ) ) ?. toString ( 'utf8' ) ;
70
+ delete sdkRequestObject . requestMappingTemplateS3Location ;
71
+ }
72
+ if ( sdkRequestObject . responseMappingTemplateS3Location ) {
73
+ sdkRequestObject . responseMappingTemplate = ( await fetchFileFromS3 ( sdkRequestObject . responseMappingTemplateS3Location , sdk ) ) ?. toString ( 'utf8' ) ;
74
+ delete sdkRequestObject . responseMappingTemplateS3Location ;
75
+ }
76
+ if ( sdkRequestObject . definitionS3Location ) {
77
+ sdkRequestObject . definition = await fetchFileFromS3 ( sdkRequestObject . definitionS3Location , sdk ) ;
78
+ delete sdkRequestObject . definitionS3Location ;
79
+ }
80
+ if ( sdkRequestObject . codeS3Location ) {
81
+ sdkRequestObject . code = await fetchFileFromS3 ( sdkRequestObject . codeS3Location , sdk ) ;
82
+ delete sdkRequestObject . codeS3Location ;
83
+ }
84
+
58
85
if ( isResolver ) {
59
86
await sdk . appsync ( ) . updateResolver ( sdkRequestObject ) . promise ( ) ;
60
- } else {
87
+ } else if ( isFunction ) {
88
+
61
89
const { functions } = await sdk . appsync ( ) . listFunctions ( { apiId : sdkRequestObject . apiId } ) . promise ( ) ;
62
90
const { functionId } = functions ?. find ( fn => fn . name === physicalName ) ?? { } ;
63
- await sdk . appsync ( ) . updateFunction ( {
64
- ...sdkRequestObject ,
65
- functionId : functionId ! ,
66
- } ) . promise ( ) ;
91
+ await simpleRetry (
92
+ ( ) => sdk . appsync ( ) . updateFunction ( { ...sdkRequestObject , functionId : functionId ! } ) . promise ( ) ,
93
+ 3 ,
94
+ 'ConcurrentModificationException' ) ;
95
+ } else {
96
+ let schemaCreationResponse : GetSchemaCreationStatusResponse = await sdk . appsync ( ) . startSchemaCreation ( sdkRequestObject ) . promise ( ) ;
97
+ while ( schemaCreationResponse . status && [ 'PROCESSING' , 'DELETING' ] . some ( status => status === schemaCreationResponse . status ) ) {
98
+ await sleep ( 1000 ) ; // poll every second
99
+ const getSchemaCreationStatusRequest : GetSchemaCreationStatusRequest = {
100
+ apiId : sdkRequestObject . apiId ,
101
+ } ;
102
+ schemaCreationResponse = await sdk . appsync ( ) . getSchemaCreationStatus ( getSchemaCreationStatusRequest ) . promise ( ) ;
103
+ }
104
+ if ( schemaCreationResponse . status === 'FAILED' ) {
105
+ throw new Error ( schemaCreationResponse . details ) ;
106
+ }
67
107
}
68
108
} ,
69
109
} ) ;
70
110
}
71
111
72
112
return ret ;
73
113
}
114
+
115
+ async function fetchFileFromS3 ( s3Url : string , sdk : ISDK ) {
116
+ const s3PathParts = s3Url . split ( '/' ) ;
117
+ const s3Bucket = s3PathParts [ 2 ] ; // first two are "s3:" and "" due to s3://
118
+ const s3Key = s3PathParts . splice ( 3 ) . join ( '/' ) ; // after removing first three we reconstruct the key
119
+ return ( await sdk . s3 ( ) . getObject ( { Bucket : s3Bucket , Key : s3Key } ) . promise ( ) ) . Body ;
120
+ }
121
+
122
+ async function simpleRetry ( fn : ( ) => Promise < any > , numOfRetries : number , errorCodeToRetry : string ) {
123
+ try {
124
+ await fn ( ) ;
125
+ } catch ( error : any ) {
126
+ if ( error && error . code === errorCodeToRetry && numOfRetries > 0 ) {
127
+ await sleep ( 500 ) ; // wait half a second
128
+ await simpleRetry ( fn , numOfRetries - 1 , errorCodeToRetry ) ;
129
+ } else {
130
+ throw error ;
131
+ }
132
+ }
133
+ }
134
+
135
+ async function sleep ( ms : number ) {
136
+ return new Promise ( ok => setTimeout ( ok , ms ) ) ;
137
+ }
0 commit comments