1
- import { ApolloClient , gql , InMemoryCache } from "@apollo/client/core/core.cjs" ;
2
- import * as graphql from '@graphql-eslint/eslint-plugin' ;
3
1
import crypto from 'crypto' ;
4
- import { Linter } from 'eslint' ;
5
- import type { Config , Context } from "@netlify/functions" ;
6
2
7
- const linter = new Linter ( { cwd : '.' , } ) ;
3
+ import { ApolloClient , InMemoryCache , gql } from '@apollo/client/core/core.cjs' ;
4
+ import * as graphql from '@graphql-eslint/eslint-plugin' ;
5
+ import type { Config , Context } from '@netlify/functions' ;
6
+ import { ESLint , Linter } from 'eslint' ;
7
+
8
+ const linter = new Linter ( { cwd : '.' } ) ;
9
+
10
+ function getSourceLocationCoordiante (
11
+ code : string ,
12
+ line : number ,
13
+ column : number ,
14
+ ) {
15
+ const lines = code . split ( '\n' ) . slice ( 0 , line ) ;
16
+ const lastLine = lines [ lines . length - 1 ] ;
17
+ return {
18
+ line,
19
+ column,
20
+ byteOffset : [ ...lines . slice ( 0 , - 1 ) , lastLine . slice ( 0 , column ) ] . join ( '\n' )
21
+ . length - 1 ,
22
+ } ;
23
+ }
8
24
9
25
const apolloClient = new ApolloClient ( {
10
- uri : Netlify . env . get ( 'APOLLO_STUDIO_URL' ) ?? 'https://api.apollographql.com/api/graphql' ,
11
- cache : new InMemoryCache ( ) ,
26
+ uri :
27
+ Netlify . env . get ( 'APOLLO_STUDIO_URL' ) ??
28
+ 'https://api.apollographql.com/api/graphql' ,
29
+ cache : new InMemoryCache ( ) ,
12
30
} ) ;
13
31
14
- const docQuery = gql `query Doc($graphId: ID!, $hash: SHA256) {
15
- graph(id: $graphId) {
16
- doc(hash: $hash) {
17
- source
32
+ const docsQuery = gql `
33
+ query CustomChecksExampleDocs($graphId: ID!, $hashes: [SHA256!]!) {
34
+ graph(id: $graphId) {
35
+ docs(hashes: $hashes) {
36
+ hash
37
+ source
38
+ }
18
39
}
19
40
}
20
- } `;
41
+ ` ;
21
42
22
- const customCheckCallbackMutation = gql `mutation CustomCheckCallback($input: CustomCheckCallbackInput!, $name: String!, $graphId: ID!) {
23
- graph(id: $graphId) {
24
- variant(name: $name) {
25
- customCheckCallback(input: $input) {
26
- __typename
27
- ... on CustomCheckResult {
28
- violations {
29
- level
43
+ const customCheckCallbackMutation = gql `
44
+ mutation CustomCheckCallback(
45
+ $input: CustomCheckCallbackInput!
46
+ $name: String!
47
+ $graphId: ID!
48
+ ) {
49
+ graph(id: $graphId) {
50
+ variant(name: $name) {
51
+ customCheckCallback(input: $input) {
52
+ __typename
53
+ ... on CustomCheckResult {
54
+ violations {
55
+ level
56
+ message
57
+ rule
58
+ }
59
+ }
60
+ ... on PermissionError {
61
+ message
62
+ }
63
+ ... on TaskError {
64
+ message
65
+ }
66
+ ... on ValidationError {
30
67
message
31
- rule
32
68
}
33
- }
34
- ... on PermissionError {
35
- message
36
- }
37
- ... on TaskError {
38
- message
39
- }
40
- ... on ValidationError {
41
- message
42
69
}
43
70
}
44
71
}
45
72
}
46
- }` ;
73
+ ` ;
74
+
75
+ interface Payload {
76
+ baseSchema : {
77
+ hash : string ;
78
+ subgraphs ?: Array < { hash : string ; name : string } > | null ;
79
+ } ;
80
+ proposedSchema : {
81
+ hash : string ;
82
+ subgraphs ?: Array < { hash : string ; name : string } > | null ;
83
+ } ;
84
+ checkStep : {
85
+ taskId : string ;
86
+ graphId : string ;
87
+ graphVariant : string ;
88
+ workflowId : string ;
89
+ } ;
90
+ gitContext : {
91
+ branch ?: string | null ;
92
+ commit ?: string | null ;
93
+ committer ?: string | null ;
94
+ message ?: string | null ;
95
+ remoteUrl ?: string | null ;
96
+ } ;
97
+ }
47
98
48
99
export default async ( req : Request , context : Context ) => {
49
100
const hmacSecret = Netlify . env . get ( 'APOLLO_HMAC_TOKEN' ) || '' ;
50
101
const apiKey = Netlify . env . get ( 'APOLLO_API_KEY' ) || '' ;
51
102
52
- const payload = await req . text ( ) || '{}' ;
103
+ const payload = ( await req . text ( ) ) || '{}' ;
53
104
console . log ( `Payload: ${ payload } ` ) ;
54
105
const providedSignature = req . headers . get ( 'x-apollo-signature' ) ;
55
106
@@ -58,90 +109,166 @@ export default async (req: Request, context: Context) => {
58
109
const calculatedSignature = `sha256=${ hmac . digest ( 'hex' ) } ` ;
59
110
60
111
if ( providedSignature === calculatedSignature ) {
61
- const event = JSON . parse ( payload ) ;
112
+ const event = JSON . parse ( payload ) as Payload ;
62
113
console . log ( `Handling taskId: ${ event . checkStep . taskId } ` ) ;
63
- const docResult = await apolloClient . query ( {
64
- query : docQuery ,
114
+ const changedSubgraphs = ( event . proposedSchema . subgraphs ?? [ ] ) . filter (
115
+ ( proposedSubgraph ) =>
116
+ event . baseSchema . subgraphs ?. find (
117
+ ( baseSubgraph ) => baseSubgraph . name === proposedSubgraph . name ,
118
+ ) ?. hash !== proposedSubgraph . hash ,
119
+ ) ;
120
+ const hashesToCheck = [
121
+ event . proposedSchema . hash ,
122
+ ...changedSubgraphs . map ( ( s ) => s . hash ) ,
123
+ ] ;
124
+ console . log ( `fetching: ${ hashesToCheck } ` ) ;
125
+ const docsResult = await apolloClient
126
+ . query < {
127
+ graph : null | {
128
+ docs : null | Array < null | { hash : string ; source : string } > ;
129
+ } ;
130
+ } > ( {
131
+ query : docsQuery ,
132
+ variables : {
133
+ graphId : event . checkStep . graphId ,
134
+ hashes : hashesToCheck ,
135
+ } ,
136
+ context : {
137
+ headers : {
138
+ 'Content-Type' : 'application/json' ,
139
+ 'apollographql-client-name' : 'custom-checks-example' ,
140
+ 'apollographql-client-version' : '0.0.1' ,
141
+ 'x-api-key' : apiKey ,
142
+ } ,
143
+ } ,
144
+ } )
145
+ . catch ( ( err ) => {
146
+ console . error ( err ) ;
147
+ return { data : { graph : null } } ;
148
+ } ) ;
149
+ const supergraphSource = docsResult . data . graph ?. docs ?. find (
150
+ ( doc ) => doc ?. hash === event . proposedSchema . hash ,
151
+ ) ?. source ;
152
+ const violations = (
153
+ await Promise . all (
154
+ changedSubgraphs . map ( async ( subgraph ) => {
155
+ const code = docsResult . data . graph ?. docs ?. find (
156
+ ( doc ) => doc ?. hash === subgraph . hash ,
157
+ ) ?. source ;
158
+ if ( typeof code !== 'string' ) {
159
+ return null ;
160
+ }
161
+ const eslingConfig : Linter . Config = {
162
+ files : [ '*.graphql' ] ,
163
+ plugins : {
164
+ '@graphql-eslint' : graphql as unknown as ESLint . Plugin ,
165
+ } ,
166
+ rules : graphql . flatConfigs [ 'schema-recommended' ]
167
+ . rules as unknown as Linter . RulesRecord ,
168
+ languageOptions : {
169
+ parser : graphql ,
170
+ parserOptions : {
171
+ graphQLConfig : { schema : supergraphSource } ,
172
+ } ,
173
+ } ,
174
+ } ;
175
+ try {
176
+ const messages = linter . verify (
177
+ code ,
178
+ eslingConfig ,
179
+ 'schema.graphql' ,
180
+ ) ;
181
+ console . log ( `eslint messages: ${ JSON . stringify ( messages ) } ` ) ;
182
+ return messages . map ( ( violation ) => {
183
+ const startSourceLocationCoordiante = getSourceLocationCoordiante (
184
+ code ,
185
+ violation . line ,
186
+ violation . column ,
187
+ ) ;
188
+ return {
189
+ level :
190
+ violation . severity === 2
191
+ ? ( 'ERROR' as const )
192
+ : ( 'WARNING' as const ) ,
193
+ message : violation . message ,
194
+ rule : violation . ruleId ?? 'unknown' ,
195
+ sourceLocations : [
196
+ {
197
+ subgraphName : subgraph . name ,
198
+ start : startSourceLocationCoordiante ,
199
+ end :
200
+ typeof violation . endLine === 'number' &&
201
+ typeof violation . endColumn === 'number'
202
+ ? getSourceLocationCoordiante (
203
+ code ,
204
+ violation . endLine ,
205
+ violation . endColumn ,
206
+ )
207
+ : startSourceLocationCoordiante ,
208
+ } ,
209
+ ] ,
210
+ } ;
211
+ } ) ;
212
+ } catch ( err ) {
213
+ console . log ( `Error: ${ err } ` ) ;
214
+ return null ;
215
+ }
216
+ } ) ,
217
+ )
218
+ ) . flat ( ) ;
219
+
220
+ console . log (
221
+ 'variables' ,
222
+ JSON . stringify ( {
223
+ graphId : event . checkStep . graphId ,
224
+ name : event . checkStep . graphVariant ,
225
+ input : {
226
+ taskId : event . checkStep . taskId ,
227
+ workflowId : event . checkStep . workflowId ,
228
+ status : violations . some (
229
+ ( violation ) => violation === null || violation . level === 'ERROR' ,
230
+ )
231
+ ? 'FAILURE'
232
+ : 'SUCCESS' ,
233
+ violations : violations . filter ( ( v ) : v is NonNullable < typeof v > => ! ! v ) ,
234
+ } ,
235
+ } ) ,
236
+ ) ;
237
+ const callbackResult = await apolloClient . mutate ( {
238
+ mutation : customCheckCallbackMutation ,
239
+ errorPolicy : 'all' ,
65
240
variables : {
66
241
graphId : event . checkStep . graphId ,
67
- // supergraph hash
68
- hash : event . proposedSchema . hash ,
242
+ name : event . checkStep . graphVariant ,
243
+ input : {
244
+ taskId : event . checkStep . taskId ,
245
+ workflowId : event . checkStep . workflowId ,
246
+ status : violations . some (
247
+ ( violation ) => violation === null || violation . level === 'ERROR' ,
248
+ )
249
+ ? 'FAILURE'
250
+ : 'SUCCESS' ,
251
+ violations : violations . filter ( ( v ) : v is NonNullable < typeof v > => ! ! v ) ,
252
+ } ,
69
253
} ,
70
254
context : {
71
255
headers : {
72
- "Content-Type" : "application/json" ,
73
- "apollographql-client-name" : "custom-checks-example" ,
74
- "apollographql-client-version" : "0.0.1" ,
75
- "x-api-key" : apiKey
76
- }
77
- }
78
- } ) ;
79
- const code = docResult . data . graph . doc . source
80
-
81
- // @ts -ignore
82
- const messages = linter . verify ( code , {
83
- files : [ '*.graphql' ] ,
84
- plugins : {
85
- '@graphql-eslint' : { rules : graphql . rules } ,
86
- } ,
87
- languageOptions : {
88
- parser : graphql ,
89
- parserOptions : {
90
- graphQLConfig : { schema : code } ,
256
+ 'Content-Type' : 'application/json' ,
257
+ 'apollographql-client-name' : 'custom-checks-example' ,
258
+ 'apollographql-client-version' : '0.0.1' ,
259
+ 'x-api-key' : apiKey ,
91
260
} ,
92
261
} ,
93
- rules : graphql . flatConfigs [ 'schema-recommended' ] . rules ,
94
- } , 'schema.graphql' ) ;
95
-
96
- console . log ( `eslint messages: ${ JSON . stringify ( messages ) } ` ) ;
97
-
98
- const violations = messages . map ( violation => ( {
99
- // Fail check if a naming convention is violated
100
- level : violation . ruleId === '@graphql-eslint/naming-convention' ? 'ERROR' : 'WARNING' ,
101
- message : violation . message ,
102
- rule : violation . ruleId ?? 'unknown' ,
103
- sourceLocations : {
104
- start : {
105
- byteOffset : 0 ,
106
- line : violation . line ,
107
- column : violation . column ,
108
- } ,
109
- end : {
110
- byteOffset : 0 ,
111
- line : violation . endLine ,
112
- column : violation . endColumn ,
113
- }
114
- }
115
- } ) ) ;
116
-
117
- const callbackResult = await apolloClient . mutate ( {
118
- mutation : customCheckCallbackMutation ,
119
- variables : {
120
- graphId : event . checkStep . graphId ,
121
- name : event . checkStep . graphVariant ,
122
- input : {
123
- taskId : event . checkStep . taskId ,
124
- workflowId : event . checkStep . workflowId ,
125
- status : violations . find ( violation => violation . level === 'ERROR' ) !== undefined ? 'FAILURE' : 'SUCCESS' ,
126
- violations : violations ,
127
- }
128
- } ,
129
- context : {
130
- headers : {
131
- "Content-Type" : "application/json" ,
132
- "apollographql-client-name" : "custom-checks-example" ,
133
- "apollographql-client-version" : "0.0.1" ,
134
- "x-api-key" : apiKey
135
- }
136
- }
137
- } ) ;
138
- console . log ( JSON . stringify ( `Callback results: ${ JSON . stringify ( callbackResult ) } ` ) ) ;
262
+ } ) ;
263
+ console . log (
264
+ JSON . stringify ( `Callback results: ${ JSON . stringify ( callbackResult ) } ` ) ,
265
+ ) ;
139
266
return new Response ( 'OK' , { status : 200 } ) ;
140
267
} else {
141
268
return new Response ( 'Signature is invalid' , { status : 403 } ) ;
142
269
}
143
270
} ;
144
271
145
272
export const config : Config = {
146
- path : '/custom-lint'
273
+ path : '/custom-lint' ,
147
274
} ;
0 commit comments