@graphql-hive/core
Version:
152 lines (151 loc) • 6.36 kB
JavaScript
import { Kind, print, separateOperations, stripIgnoredCharacters, TypeInfo, visit, } from 'graphql';
import { md5 } from 'js-md5';
import sortBy from 'lodash.sortby';
import { collectSchemaCoordinates } from '../client/collect-schema-coordinates.js';
/**
* Normalize a operation document.
*/
export function normalizeOperation({ document, operationName, hideLiterals = true, removeAliases = true, }) {
var _a, _b;
return stripIgnoredCharacters(print(visit(dropUnusedDefinitions(document, operationName !== null && operationName !== void 0 ? operationName : (_b = (_a = document.definitions.find(isOperationDef)) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.value), {
// hide literals
IntValue(node) {
return hideLiterals ? Object.assign(Object.assign({}, node), { value: '0' }) : node;
},
FloatValue(node) {
return hideLiterals ? Object.assign(Object.assign({}, node), { value: '0' }) : node;
},
StringValue(node) {
return hideLiterals ? Object.assign(Object.assign({}, node), { value: '', block: false }) : node;
},
Field(node) {
return Object.assign(Object.assign({}, node), {
// remove aliases
alias: removeAliases ? undefined : node.alias,
// sort arguments
arguments: sortNodes(node.arguments) });
},
Document(node) {
return Object.assign(Object.assign({}, node), { definitions: sortNodes(node.definitions) });
},
OperationDefinition(node) {
return Object.assign(Object.assign({}, node), { variableDefinitions: sortNodes(node.variableDefinitions) });
},
SelectionSet(node) {
return Object.assign(Object.assign({}, node), { selections: sortNodes(node.selections) });
},
FragmentSpread(node) {
return Object.assign(Object.assign({}, node), { directives: sortNodes(node.directives) });
},
InlineFragment(node) {
return Object.assign(Object.assign({}, node), { directives: sortNodes(node.directives) });
},
FragmentDefinition(node) {
return Object.assign(Object.assign({}, node), { directives: sortNodes(node.directives), variableDefinitions: sortNodes(node.variableDefinitions) });
},
Directive(node) {
return Object.assign(Object.assign({}, node), { arguments: sortNodes(node.arguments) });
},
})));
}
function sortNodes(nodes) {
if (nodes) {
if (nodes.length === 0) {
return [];
}
if (isOfKindList(nodes, Kind.DIRECTIVE)) {
return sortBy(nodes, 'name.value');
}
if (isOfKindList(nodes, Kind.VARIABLE_DEFINITION)) {
return sortBy(nodes, 'variable.name.value');
}
if (isOfKindList(nodes, Kind.ARGUMENT)) {
return sortBy(nodes, 'name.value');
}
if (isOfKindList(nodes, [Kind.FIELD, Kind.FRAGMENT_SPREAD, Kind.INLINE_FRAGMENT])) {
return sortBy(nodes, 'kind', 'name.value');
}
return sortBy(nodes, 'kind', 'name.value');
}
return;
}
function isOfKindList(nodes, kind) {
return typeof kind === 'string' ? nodes[0].kind === kind : kind.includes(nodes[0].kind);
}
function isOperationDef(def) {
return def.kind === Kind.OPERATION_DEFINITION;
}
function dropUnusedDefinitions(doc, operationName) {
var _a;
if (!operationName) {
return doc;
}
return (_a = separateOperations(doc)[operationName]) !== null && _a !== void 0 ? _a : doc;
}
function findOperationDefinition(doc) {
return doc.definitions.find(isOperationDef);
}
/** normalize a graphql operation into a stable hash as used internally within our ClickHouse Database. */
export function preprocessOperation(operation) {
var _a, _b;
const body = normalizeOperation({
document: operation.document,
hideLiterals: true,
removeAliases: true,
});
// Two operations with the same hash has to be equal:
// 1. body is the same
// 2. name is the same
// 3. used schema coordinates are equal - this is important to assign schema coordinate to an operation
const uniqueCoordinatesSet = new Set();
for (const field of operation.schemaCoordinates) {
uniqueCoordinatesSet.add(field);
// Add types as well:
// `Query.foo` -> `Query`
const at = field.indexOf('.');
if (at > -1) {
uniqueCoordinatesSet.add(field.substring(0, at));
}
}
const sortedCoordinates = Array.from(uniqueCoordinatesSet).sort();
const operationDefinition = findOperationDefinition(operation.document);
if (!operationDefinition) {
return null;
}
const operationName = (_a = operation.operationName) !== null && _a !== void 0 ? _a : (_b = operationDefinition.name) === null || _b === void 0 ? void 0 : _b.value;
const hash = md5
.create()
.update(body)
.update(operationName !== null && operationName !== void 0 ? operationName : '')
.update(sortedCoordinates.join(';')) // we do not need to sort from A to Z, default lexicographic sorting is enough
.hex();
return {
type: operationDefinition.operation,
hash,
body,
coordinates: sortedCoordinates,
name: operationName || null,
};
}
/**
* Hash a executable GraphQL document according to Hive platforms algorithm
* for identification.
*
* Return null if no executable operation definition was found.
*/
export function hashOperation(args) {
var _a, _b, _c;
const schemaCoordinates = collectSchemaCoordinates({
documentNode: args.documentNode,
processVariables: args.variables !== null,
variables: (_a = args.variables) !== null && _a !== void 0 ? _a : {},
schema: args.schema,
typeInfo: (_b = args.typeInfo) !== null && _b !== void 0 ? _b : new TypeInfo(args.schema),
});
const result = preprocessOperation({
document: args.documentNode,
schemaCoordinates: schemaCoordinates,
operationName: args.operationName,
});
return (_c = result === null || result === void 0 ? void 0 : result.hash) !== null && _c !== void 0 ? _c : null;
}