graphql-shield
Version:
GraphQL Server permissions as another layer of abstraction!
180 lines (179 loc) • 7.11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReplaceFieldWithFragment = void 0;
// copied from https://github.com/ardatan/graphql-tools/blob/a233614ea5e76b855184c6d374e164904ff2ecb1/src/transforms/ReplaceFieldWithFragment.ts
// TODO: return it back to graphql-tools?
const graphql_1 = require("graphql");
function deduplicateSelection(nodes) {
const selectionMap = nodes.reduce((map, node) => {
switch (node.kind) {
case 'Field': {
if (node.alias != null) {
if (node.alias.value in map) {
return map;
}
return {
...map,
[node.alias.value]: node,
};
}
if (node.name.value in map) {
return map;
}
return {
...map,
[node.name.value]: node,
};
}
case 'FragmentSpread': {
if (node.name.value in map) {
return map;
}
return {
...map,
[node.name.value]: node,
};
}
case 'InlineFragment': {
if (map.__fragment != null) {
const fragment = map.__fragment;
const typeCondition = fragment.typeCondition;
if (!typeCondition) {
throw new Error(`Fragment ${fragment} does not have a type condition`);
}
return {
...map,
__fragment: concatInlineFragments(typeCondition.name.value, [fragment, node]),
};
}
return {
...map,
__fragment: node,
};
}
default: {
return map;
}
}
}, Object.create(null));
const selection = Object.keys(selectionMap).reduce((selectionList, node) => selectionList.concat(selectionMap[node]), []);
return selection;
}
function concatInlineFragments(type, fragments) {
const fragmentSelections = fragments.reduce((selections, fragment) => selections.concat(fragment.selectionSet.selections), []);
const deduplicatedFragmentSelection = deduplicateSelection(fragmentSelections);
return {
kind: graphql_1.Kind.INLINE_FRAGMENT,
typeCondition: {
kind: graphql_1.Kind.NAMED_TYPE,
name: {
kind: graphql_1.Kind.NAME,
value: type,
},
},
selectionSet: {
kind: graphql_1.Kind.SELECTION_SET,
selections: deduplicatedFragmentSelection,
},
};
}
class ReplaceFieldWithFragment {
constructor(fragments) {
this.mapping = {};
for (const { field, fragment } of fragments) {
const parsedFragment = parseFragmentToInlineFragment(fragment);
const typeCondition = parsedFragment.typeCondition;
if (!typeCondition) {
throw new Error(`Fragment ${fragment} does not have a type condition`);
}
const actualTypeName = typeCondition.name.value;
if (!(actualTypeName in this.mapping)) {
this.mapping[actualTypeName] = Object.create(null);
}
const typeMapping = this.mapping[actualTypeName];
if (!(field in typeMapping)) {
typeMapping[field] = [parsedFragment];
}
else {
typeMapping[field].push(parsedFragment);
}
}
}
transformSchema(originalSchema) {
this.targetSchema = originalSchema;
return originalSchema;
}
transformRequest(originalRequest) {
if (!this.targetSchema) {
throw new Error(`The ReplaceFieldWithFragment transform's "transformRequest" and "transformResult" methods cannot be used without first calling "transformSchema".`);
}
const document = replaceFieldsWithFragments(this.targetSchema, originalRequest.document, this.mapping);
return {
...originalRequest,
document,
};
}
}
exports.ReplaceFieldWithFragment = ReplaceFieldWithFragment;
function replaceFieldsWithFragments(targetSchema, document, mapping) {
const typeInfo = new graphql_1.TypeInfo(targetSchema);
return (0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
[graphql_1.Kind.INLINE_FRAGMENT](node) {
// do not visit fragments added by this transform
if (node.__isShieldInternalFragment) {
return false;
}
return undefined;
},
[graphql_1.Kind.SELECTION_SET](node) {
const parentType = typeInfo.getParentType();
if (parentType != null) {
const parentTypeName = parentType.name;
let selections = node.selections;
if (parentTypeName in mapping) {
node.selections.forEach((selection) => {
if (selection.kind === graphql_1.Kind.FIELD) {
const name = selection.name.value;
const fragments = mapping[parentTypeName][name];
if (fragments != null && fragments.length > 0) {
const fragment = {
...concatInlineFragments(parentTypeName, fragments),
__isShieldInternalFragment: true,
};
selections = selections.concat(fragment);
}
}
});
}
if (selections !== node.selections) {
return {
...node,
selections: deduplicateSelection([...selections]),
};
}
}
return undefined;
},
}));
}
function parseFragmentToInlineFragment(definitions) {
if (definitions.trim().startsWith('fragment')) {
const document = (0, graphql_1.parse)(definitions);
for (const definition of document.definitions) {
if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) {
return {
kind: graphql_1.Kind.INLINE_FRAGMENT,
typeCondition: definition.typeCondition,
selectionSet: definition.selectionSet,
};
}
}
}
const query = (0, graphql_1.parse)(`{${definitions}}`).definitions[0];
for (const selection of query.selectionSet.selections) {
if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) {
return selection;
}
}
throw new Error('Could not parse fragment');
}