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

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

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

Changed files
+73 -30
.changeset
packages
+5
.changeset/many-nails-wait.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Add validation step to check that the persisted-operations hash has been updated when the document changes
+25
packages/graphqlsp/src/diagnostics.ts
···
getColocatedFragmentNames,
} from './checkImports';
import {
+
generateHashForDocument,
getDocumentReferenceFromDocumentNode,
getDocumentReferenceFromTypeQuery,
} from './persisted';
···
export const MISSING_PERSISTED_TYPE_ARG = 520100;
export const MISSING_PERSISTED_CODE_ARG = 520101;
export const MISSING_PERSISTED_DOCUMENT = 520102;
+
export const MISSMATCH_HASH_TO_DOCUMENT = 520103;
export const ALL_DIAGNOSTICS = [
SEMANTIC_DIAGNOSTIC_CODE,
MISSING_OPERATION_NAME_CODE,
···
MISSING_PERSISTED_TYPE_ARG,
MISSING_PERSISTED_CODE_ARG,
MISSING_PERSISTED_DOCUMENT,
+
MISSMATCH_HASH_TO_DOCUMENT,
];
const cache = new LRUCache<number, ts.Diagnostic[]>({
···
start: callExpression.arguments.pos,
length: callExpression.arguments.end - callExpression.arguments.pos,
};
+
}
+
+
const hash = callExpression.arguments[0].getText().slice(1, -1);
+
if (hash.startsWith('sha256:')) {
+
const hash = generateHashForDocument(
+
info,
+
initializer.arguments[0],
+
foundFilename
+
);
+
if (!hash) return null;
+
const upToDateHash = `sha256:${hash}`;
+
if (upToDateHash !== hash) {
+
return {
+
category: ts.DiagnosticCategory.Warning,
+
code: MISSMATCH_HASH_TO_DOCUMENT,
+
file: source,
+
messageText: `The persisted document's hash is outdated`,
+
start: callExpression.arguments.pos,
+
length:
+
callExpression.arguments.end - callExpression.arguments.pos,
+
};
+
}
}
return null;
+43 -30
packages/graphqlsp/src/persisted.ts
···
)
return undefined;
-
const externalSource = getSource(info, foundFilename)!;
-
const { fragments } = findAllCallExpressions(externalSource, info);
-
-
const text = resolveTemplate(
+
const hash = generateHashForDocument(
+
info,
initializer.arguments[0],
-
foundFilename,
-
info
-
).combinedText;
-
const parsed = parse(text);
-
const spreads = new Set();
-
visit(parsed, {
-
FragmentSpread: node => {
-
spreads.add(node.name.value);
-
},
-
});
-
-
let resolvedText = text;
-
[...spreads].forEach(spreadName => {
-
const fragmentDefinition = fragments.find(x => x.name.value === spreadName);
-
if (!fragmentDefinition) {
-
console.warn(
-
`[GraphQLSP] could not find fragment for spread ${spreadName}!`
-
);
-
return;
-
}
-
-
resolvedText = `${resolvedText}\n\n${print(fragmentDefinition)}`;
-
});
-
-
const hash = createHash('sha256').update(text).digest('hex');
-
+
foundFilename
+
);
const existingHash = callExpression.arguments[0];
// We assume for now that this is either undefined or an existing string literal
if (!existingHash) {
···
return undefined;
}
}
+
+
export const generateHashForDocument = (
+
info: ts.server.PluginCreateInfo,
+
templateLiteral:
+
| ts.NoSubstitutionTemplateLiteral
+
| ts.TaggedTemplateExpression,
+
foundFilename: string
+
): string | undefined => {
+
const externalSource = getSource(info, foundFilename)!;
+
const { fragments } = findAllCallExpressions(externalSource, info);
+
+
const text = resolveTemplate(
+
templateLiteral,
+
foundFilename,
+
info
+
).combinedText;
+
const parsed = parse(text);
+
const spreads = new Set();
+
visit(parsed, {
+
FragmentSpread: node => {
+
spreads.add(node.name.value);
+
},
+
});
+
+
let resolvedText = text;
+
[...spreads].forEach(spreadName => {
+
const fragmentDefinition = fragments.find(x => x.name.value === spreadName);
+
if (!fragmentDefinition) {
+
console.warn(
+
`[GraphQLSP] could not find fragment for spread ${spreadName}!`
+
);
+
return;
+
}
+
+
resolvedText = `${resolvedText}\n\n${print(fragmentDefinition)}`;
+
});
+
+
return createHash('sha256').update(text).digest('hex');
+
};
export const getDocumentReferenceFromTypeQuery = (
typeQuery: ts.TypeQueryNode,