Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(diagnostics): validate up to date hash (#301)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
5865e2a1 4b88add6

+73 -30
+5
.changeset/many-nails-wait.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Add validation step to check that the persisted-operations hash has been updated when the document changes
+25
packages/graphqlsp/src/diagnostics.ts
··· 25 25 getColocatedFragmentNames, 26 26 } from './checkImports'; 27 27 import { 28 + generateHashForDocument, 28 29 getDocumentReferenceFromDocumentNode, 29 30 getDocumentReferenceFromTypeQuery, 30 31 } from './persisted'; ··· 54 55 export const MISSING_PERSISTED_TYPE_ARG = 520100; 55 56 export const MISSING_PERSISTED_CODE_ARG = 520101; 56 57 export const MISSING_PERSISTED_DOCUMENT = 520102; 58 + export const MISSMATCH_HASH_TO_DOCUMENT = 520103; 57 59 export const ALL_DIAGNOSTICS = [ 58 60 SEMANTIC_DIAGNOSTIC_CODE, 59 61 MISSING_OPERATION_NAME_CODE, ··· 63 65 MISSING_PERSISTED_TYPE_ARG, 64 66 MISSING_PERSISTED_CODE_ARG, 65 67 MISSING_PERSISTED_DOCUMENT, 68 + MISSMATCH_HASH_TO_DOCUMENT, 66 69 ]; 67 70 68 71 const cache = new LRUCache<number, ts.Diagnostic[]>({ ··· 245 248 start: callExpression.arguments.pos, 246 249 length: callExpression.arguments.end - callExpression.arguments.pos, 247 250 }; 251 + } 252 + 253 + const hash = callExpression.arguments[0].getText().slice(1, -1); 254 + if (hash.startsWith('sha256:')) { 255 + const hash = generateHashForDocument( 256 + info, 257 + initializer.arguments[0], 258 + foundFilename 259 + ); 260 + if (!hash) return null; 261 + const upToDateHash = `sha256:${hash}`; 262 + if (upToDateHash !== hash) { 263 + return { 264 + category: ts.DiagnosticCategory.Warning, 265 + code: MISSMATCH_HASH_TO_DOCUMENT, 266 + file: source, 267 + messageText: `The persisted document's hash is outdated`, 268 + start: callExpression.arguments.pos, 269 + length: 270 + callExpression.arguments.end - callExpression.arguments.pos, 271 + }; 272 + } 248 273 } 249 274 250 275 return null;
+43 -30
packages/graphqlsp/src/persisted.ts
··· 119 119 ) 120 120 return undefined; 121 121 122 - const externalSource = getSource(info, foundFilename)!; 123 - const { fragments } = findAllCallExpressions(externalSource, info); 124 - 125 - const text = resolveTemplate( 122 + const hash = generateHashForDocument( 123 + info, 126 124 initializer.arguments[0], 127 - foundFilename, 128 - info 129 - ).combinedText; 130 - const parsed = parse(text); 131 - const spreads = new Set(); 132 - visit(parsed, { 133 - FragmentSpread: node => { 134 - spreads.add(node.name.value); 135 - }, 136 - }); 137 - 138 - let resolvedText = text; 139 - [...spreads].forEach(spreadName => { 140 - const fragmentDefinition = fragments.find(x => x.name.value === spreadName); 141 - if (!fragmentDefinition) { 142 - console.warn( 143 - `[GraphQLSP] could not find fragment for spread ${spreadName}!` 144 - ); 145 - return; 146 - } 147 - 148 - resolvedText = `${resolvedText}\n\n${print(fragmentDefinition)}`; 149 - }); 150 - 151 - const hash = createHash('sha256').update(text).digest('hex'); 152 - 125 + foundFilename 126 + ); 153 127 const existingHash = callExpression.arguments[0]; 154 128 // We assume for now that this is either undefined or an existing string literal 155 129 if (!existingHash) { ··· 187 161 return undefined; 188 162 } 189 163 } 164 + 165 + export const generateHashForDocument = ( 166 + info: ts.server.PluginCreateInfo, 167 + templateLiteral: 168 + | ts.NoSubstitutionTemplateLiteral 169 + | ts.TaggedTemplateExpression, 170 + foundFilename: string 171 + ): string | undefined => { 172 + const externalSource = getSource(info, foundFilename)!; 173 + const { fragments } = findAllCallExpressions(externalSource, info); 174 + 175 + const text = resolveTemplate( 176 + templateLiteral, 177 + foundFilename, 178 + info 179 + ).combinedText; 180 + const parsed = parse(text); 181 + const spreads = new Set(); 182 + visit(parsed, { 183 + FragmentSpread: node => { 184 + spreads.add(node.name.value); 185 + }, 186 + }); 187 + 188 + let resolvedText = text; 189 + [...spreads].forEach(spreadName => { 190 + const fragmentDefinition = fragments.find(x => x.name.value === spreadName); 191 + if (!fragmentDefinition) { 192 + console.warn( 193 + `[GraphQLSP] could not find fragment for spread ${spreadName}!` 194 + ); 195 + return; 196 + } 197 + 198 + resolvedText = `${resolvedText}\n\n${print(fragmentDefinition)}`; 199 + }); 200 + 201 + return createHash('sha256').update(text).digest('hex'); 202 + }; 190 203 191 204 export const getDocumentReferenceFromTypeQuery = ( 192 205 typeQuery: ts.TypeQueryNode,