Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | 128x 128x 128x 55x 128x 49x 128x 38x 38x 38x 64x 55x 55x 55x 45x 45x 20x 20x 19x 44x 44x 9x 8x 8x 6x 6x 5x 5x 1x 38x 12x 13x 12x 12x 12x 12x 12x 10x 7x 12x 13x 10x 12x 128x 128x 128x 30x 30x 30x 30x 30x 30x | import {
DirectiveNode,
DocumentNode,
Kind,
OperationDefinitionNode,
SelectionNode,
SelectionSetNode,
visit,
} from 'graphql';
import type { SchemaFieldIndex } from '../services/SchemaIntrospector';
/**
* Client-side directive marking a field as non-essential: when the connected
* instance's schema lacks it, the field is dropped instead of failing the whole
* query. Never sent to GitLab.
*/
export const OPTIONAL_DIRECTIVE = 'optional';
export interface PreparedDocument {
document: DocumentNode;
/** Variables the prepared document still declares; others must not be sent. */
variableNames: ReadonlySet<string>;
}
const isOptional = (directives?: readonly DirectiveNode[]): boolean =>
directives?.some((d) => d.name.value === OPTIONAL_DIRECTIVE) ?? false;
const stripOptional = (directives?: readonly DirectiveNode[]): DirectiveNode[] | undefined =>
directives?.filter((d) => d.name.value !== OPTIONAL_DIRECTIVE);
/**
* Selection for a kept field (or operation) whose every sub-selection was
* dropped: a composite type needs at least one field, and __typename exists on
* every type.
*/
const typenameOnly = (set: SelectionSetNode): SelectionSetNode => ({
...set,
selections: [{ kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename' } }],
});
/**
* Drop what the instance cannot answer. An inline fragment on a type the schema
* does not declare can never match, so it always goes. A missing field goes only
* when marked @optional; an essential missing field is left in place so GitLab
* reports it and the caller can fall back or fail loudly. Returns undefined when
* nothing selectable remains.
*/
function pruneSelectionSet(
set: SelectionSetNode,
typeName: string | undefined,
index: SchemaFieldIndex | undefined,
): SelectionSetNode | undefined {
const fields = typeName ? index?.get(typeName) : undefined;
const selections: SelectionNode[] = [];
for (const selection of set.selections) {
if (selection.kind === Kind.FIELD) {
const optional = isOptional(selection.directives);
const field = fields?.get(selection.name.value);
if (optional && fields && !field && selection.name.value !== '__typename') continue;
let selectionSet = selection.selectionSet;
if (selectionSet) {
const pruned = pruneSelectionSet(selectionSet, field?.type, index);
if (!pruned && optional) continue;
selectionSet = pruned ?? typenameOnly(selectionSet);
}
selections.push({
...selection,
directives: stripOptional(selection.directives),
selectionSet,
});
continue;
}
if (selection.kind === Kind.INLINE_FRAGMENT) {
const condition = selection.typeCondition?.name.value;
if (condition && index && !index.has(condition)) continue;
const pruned = pruneSelectionSet(selection.selectionSet, condition ?? typeName, index);
if (!pruned) continue;
selections.push({
...selection,
directives: stripOptional(selection.directives),
selectionSet: pruned,
});
continue;
}
selections.push(selection);
}
return selections.length > 0 ? { ...set, selections } : undefined;
}
function prepare(document: DocumentNode, index: SchemaFieldIndex | undefined): PreparedDocument {
const pruned: DocumentNode = {
...document,
definitions: document.definitions.map((definition) => {
if (definition.kind !== Kind.OPERATION_DEFINITION) return definition;
const root = definition.operation === 'mutation' ? 'Mutation' : 'Query';
const selectionSet =
pruneSelectionSet(definition.selectionSet, root, index) ??
typenameOnly(definition.selectionSet);
return { ...definition, selectionSet };
}),
};
// Variables referenced only by pruned selections must not stay declared:
// GitLab rejects a declared-but-unused variable.
const used = new Set<string>();
visit(pruned, {
VariableDefinition: () => false,
Variable: (node) => {
used.add(node.name.value);
},
});
const result: DocumentNode = {
...pruned,
definitions: pruned.definitions.map((definition) =>
definition.kind === Kind.OPERATION_DEFINITION
? ({
...definition,
variableDefinitions: definition.variableDefinitions?.filter((v) =>
used.has(v.variable.name.value),
),
} satisfies OperationDefinitionNode)
: definition,
),
};
return { document: result, variableNames: used };
}
// Prepared documents are pure functions of (document, schema); both are
// long-lived, so memoise per pair without pinning either in memory.
const NO_SCHEMA = {};
const cache = new WeakMap<DocumentNode, WeakMap<object, PreparedDocument>>();
/**
* Adapt a document to the instance schema (see pruneSelectionSet) and strip the
* client-only @optional directive. Without a schema index nothing is pruned.
*/
export function prepareDocument(
document: DocumentNode,
index: SchemaFieldIndex | undefined,
): PreparedDocument {
const key = index ?? NO_SCHEMA;
let perSchema = cache.get(document);
if (!perSchema) cache.set(document, (perSchema = new WeakMap()));
let prepared = perSchema.get(key);
if (!prepared) perSchema.set(key, (prepared = prepare(document, index)));
return prepared;
}
|