• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rubensworks / graphql-to-sparql.js / 20781762490

07 Jan 2026 12:38PM UTC coverage: 92.605% (-0.05%) from 92.659%
20781762490

push

github

rubensworks
Update to Traqula v1

328 of 363 branches covered (90.36%)

Branch coverage included in aggregate %.

511 of 543 relevant lines covered (94.11%)

312.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.74
/lib/handler/NodeHandlerSelectionAdapter.ts
1
import {FieldNode, IntValueNode, SelectionNode} from "graphql/language";
2
import * as RDF from "@rdfjs/types";
3
import type {Algebra} from "@traqula/algebra-transformations-1-2";
4
import {algebraUtils} from "@traqula/algebra-transformations-1-2";
48✔
5
import {IConvertContext, SingularizeState} from "../IConvertContext";
48✔
6
import {IConvertSettings} from "../IConvertSettings";
7
import {Util} from "../Util";
8
import {INodeQuadContext, NodeHandlerAdapter} from "./NodeHandlerAdapter";
48✔
9

10
/**
11
 * A handler for converting GraphQL selection nodes to operations.
12
 */
13
export abstract class NodeHandlerSelectionAdapter<T extends SelectionNode> extends NodeHandlerAdapter<T> {
48✔
14

15
  constructor(targetKind: T['kind'], util: Util, settings: IConvertSettings) {
16
    super(targetKind, util, settings);
1,038✔
17
  }
18

19
  /**
20
   * Get the quad context of a field node that should be used for the whole definition node.
21
   * @param {FieldNode} field A field node.
22
   * @param {string} fieldLabel A field label.
23
   * @param {IConvertContext} convertContext A convert context.
24
   * @return {INodeQuadContext} The subject and optional auxiliary patterns.
25
   */
26
  public getNodeQuadContextFieldNode(field: FieldNode, fieldLabel: string, convertContext: IConvertContext)
27
    : INodeQuadContext {
28
    return this.getNodeQuadContextSelectionSet(field.selectionSet, fieldLabel, {
762✔
29
      ...convertContext,
30
      path: this.util.appendFieldToPath(convertContext.path, fieldLabel),
31
    });
32
  }
33

34
  /**
35
   * Convert a field node to an operation.
36
   * @param {IConvertContext} convertContext A convert context.
37
   * @param {FieldNode} fieldNode The field node to convert.
38
   * @param {boolean} pushTerminalVariables If terminal variables should be created.
39
   * @param {Pattern[]} auxiliaryPatterns Optional patterns that should be part of the BGP.
40
   * @return {Operation} The reslting operation.
41
   */
42
  public fieldToOperation(convertContext: IConvertContext, fieldNode: FieldNode,
43
                          pushTerminalVariables: boolean, auxiliaryPatterns?: Algebra.Pattern[]): Algebra.Operation {
44
    // If a deeper node is being selected, and if the current object should become the next subject
45
    const nesting = pushTerminalVariables;
774✔
46

47
    // Offset and limit can be changed using the magic arguments 'first' and 'offset'.
48
    let offset = 0;
774✔
49
    let limit;
50

51
    // Ignore 'id' and 'graph' fields, because we have processed them earlier in getNodeQuadContextSelectionSet.
52
    if (fieldNode.name.value === 'id' || fieldNode.name.value === 'graph') {
774✔
53
      pushTerminalVariables = false;
48✔
54

55
      // Validate all _-arguments, because even though they were handled before,
56
      // the validity of variables could not be checked,
57
      // as variablesMetaDict wasn't populated at that time yet.
58
      if (fieldNode.arguments) {
48!
59
        for (const argument of fieldNode.arguments) {
48✔
60
          if (argument.name.value === '_') {
24!
61
            this.util.handleNodeValue(argument.value, fieldNode.name.value, convertContext);
24✔
62
          }
63
        }
64
      }
65
    }
66

67
    // Determine the field label for variable naming, taking into account aliases
68
    const fieldLabel: string = this.util.getFieldLabel(fieldNode);
768✔
69

70
    // Handle the singular/plural scope
71
    if (convertContext.singularizeState === SingularizeState.SINGLE) {
768✔
72
      convertContext.singularizeVariables![this.util.nameToVariable(fieldLabel, convertContext).value] = true;
54✔
73
    }
74

75
    // Handle meta fields
76
    if (pushTerminalVariables) {
768✔
77
      const operationOverride = this.handleMetaField(convertContext, fieldLabel, auxiliaryPatterns);
690✔
78
      if (operationOverride) {
690✔
79
        return operationOverride;
6✔
80
      }
81
    }
82

83
    const operations: Algebra.Operation[] = auxiliaryPatterns
762✔
84
      ? [this.util.operationFactory.createBgp(auxiliaryPatterns)] : [];
85

86
    // Define subject and object
87
    const subjectOutput = this.getNodeQuadContextFieldNode(fieldNode, fieldLabel, convertContext);
762✔
88
    let object: RDF.Term = subjectOutput.subject || this.util.nameToVariable(fieldLabel, convertContext);
762✔
89
    let graph: RDF.Term = subjectOutput.graph || convertContext.graph;
762✔
90
    if (subjectOutput.auxiliaryPatterns) {
762✔
91
      operations.push(this.util.operationFactory.createBgp(subjectOutput.auxiliaryPatterns));
×
92
    }
93

94
    // Check if there is a '_' argument
95
    // We do this before handling all other arguments so that the order of final triple patterns is sane.
96
    let createQuadPattern: boolean = true;
762✔
97
    let overrideObjectTerms: RDF.Term[] | null = null;
762✔
98
    if (pushTerminalVariables && fieldNode.arguments && fieldNode.arguments.length) {
762✔
99
      for (const argument of fieldNode.arguments) {
150✔
100
        if (argument.name.value === '_') {
162✔
101
          // '_'-arguments do not create an additional predicate link, but set the value directly.
102
          const valueOutput = this.util.handleNodeValue(argument.value, fieldNode.name.value, convertContext);
24✔
103
          overrideObjectTerms = valueOutput.terms;
18✔
104
          operations.push(this.util.operationFactory.createBgp(
18✔
105
            valueOutput.terms.map((term) => this.util.createQuadPattern(
18✔
106
              convertContext.subject, fieldNode.name, term, convertContext.graph, convertContext.context)),
107
          ));
108
          if (valueOutput.auxiliaryPatterns) {
18!
109
            operations.push(this.util.operationFactory.createBgp(valueOutput.auxiliaryPatterns));
×
110
          }
111
          pushTerminalVariables = false;
18✔
112
          break;
18✔
113
        } else if (argument.name.value === 'graph') {
138✔
114
          // 'graph'-arguments do not create an additional predicate link, but set the graph.
115
          const valueOutput = this.util.handleNodeValue(argument.value, fieldNode.name.value, convertContext);
12✔
116
          if (valueOutput.terms.length !== 1) {
12!
117
            throw new Error(`Only single values can be set as graph, but got ${valueOutput.terms
×
118
              .length} at ${fieldNode.name.value}`);
119
          }
120
          graph = valueOutput.terms[0];
12✔
121
          convertContext = { ...convertContext, graph };
12✔
122
          if (valueOutput.auxiliaryPatterns) {
12!
123
            operations.push(this.util.operationFactory.createBgp(valueOutput.auxiliaryPatterns));
×
124
          }
125
          break;
12✔
126
        } else if (argument.name.value === 'alt') {
126✔
127
          // 'alt'-arguments do not create an additional predicate link, but create alt-property paths.
128

129
          let pathValue = argument.value;
30✔
130
          if (pathValue.kind !== 'ListValue') {
30✔
131
            pathValue = { kind: 'ListValue', values: [ pathValue ] };
18✔
132
          }
133

134
          operations.push(this.util.createQuadPath(convertContext.subject, fieldNode.name, pathValue, object,
30✔
135
            convertContext.graph, convertContext.context));
136
          createQuadPattern = false;
24✔
137

138
          break;
24✔
139
        }
140
      }
141
    }
142

143
    // Create at least a pattern for the parent node and the current path.
144
    if (pushTerminalVariables && createQuadPattern) {
750✔
145
      operations.push(this.util.operationFactory.createBgp([
630✔
146
        this.util.createQuadPattern(convertContext.subject, fieldNode.name, object,
147
          convertContext.graph, convertContext.context),
148
      ]));
149
    }
150

151
    // Create patterns for the node's arguments
152
    if (fieldNode.arguments && fieldNode.arguments.length) {
750✔
153
      for (const argument of fieldNode.arguments) {
156✔
154
        if (argument.name.value === '_' || argument.name.value === 'graph' || argument.name.value === 'alt') {
168✔
155
          // no-op
156
        } else if (argument.name.value === 'first') {
96✔
157
          if (argument.value.kind !== 'IntValue') {
6!
158
            throw new Error('Invalid value type for \'first\' argument: ' + argument.value.kind);
×
159
          }
160
          limit = parseInt((<IntValueNode> argument.value).value, 10);
6✔
161
        } else if (argument.name.value === 'offset') {
90✔
162
          if (argument.value.kind !== 'IntValue') {
6!
163
            throw new Error('Invalid value type for \'offset\' argument: ' + argument.value.kind);
×
164
          }
165
          offset = parseInt((<IntValueNode> argument.value).value, 10);
6✔
166
        } else {
167
          const valueOutput = this.util.handleNodeValue(argument.value, argument.name.value, convertContext);
84✔
168
          operations.push(this.util.operationFactory.createBgp(
84✔
169
            valueOutput.terms.map((term) => this.util.createQuadPattern(
84✔
170
              object, argument.name, term, convertContext.graph, convertContext.context)),
171
          ));
172
          if (valueOutput.auxiliaryPatterns) {
84!
173
            operations.push(this.util.operationFactory.createBgp(valueOutput.auxiliaryPatterns));
×
174
          }
175
        }
176
      }
177
    }
178

179
    // Directives
180
    const directiveOutputs = this.getDirectiveOutputs(fieldNode.directives, fieldLabel, convertContext);
750✔
181
    if (!directiveOutputs) {
750✔
182
      return this.util.operationFactory.createBgp([]);
6✔
183
    }
184

185
    // Recursive call for nested selection sets
186
    let operation: Algebra.Operation = this.util.joinOperations(operations);
744✔
187
    if (fieldNode.selectionSet && fieldNode.selectionSet.selections.length) {
744✔
188
      // Override the object if needed
189
      if (overrideObjectTerms) {
312✔
190
        if (overrideObjectTerms.length !== 1) {
6!
191
          throw new Error(`Only single values can be set as id, but got ${overrideObjectTerms
×
192
            .length} at ${fieldNode.name.value}`);
193
        }
194
        object = overrideObjectTerms[0];
6✔
195
      }
196

197
      // Change path value when there was an alias on this node.
198
      const subConvertContext: IConvertContext = {
312✔
199
        ...convertContext,
200
        ...nesting ? { path: this.util.appendFieldToPath(convertContext.path, fieldLabel) } : {},
156✔
201
        graph,
202
        subject: nesting ? object : convertContext.subject,
156✔
203
      };
204

205
      // If the magic keyword 'totalCount' is present, include a count aggregator.
206
      let totalCount: boolean = false;
312✔
207
      const selections: ReadonlyArray<SelectionNode> = fieldNode.selectionSet.selections
312✔
208
        .filter((selection) => {
209
          if (selection.kind === 'Field' && selection.name.value === 'totalCount') {
438✔
210
            totalCount = true;
12✔
211
            return false;
12✔
212
          }
213
          return true;
426✔
214
        });
215

216
      let joinedOperation = this.util.joinOperations(operations
312✔
217
        .concat(selections.map((selectionNode) => this.util.handleNode(selectionNode, subConvertContext))));
426✔
218

219
      // Modify the operation if there was a count selection
220
      if (totalCount) {
306✔
221
        // Create to a count aggregation
222
        const expressionVariable = this.util.dataFactory.variable!('var' + this.settings.expressionVariableCounter!++);
12✔
223
        const countOverVariable: RDF.Variable = this.util.dataFactory
12✔
224
          .variable!(object.value + this.settings.variableDelimiter + 'totalCount');
225
        const aggregator: Algebra.BoundAggregate = this.util.operationFactory.createBoundAggregate(expressionVariable,
12✔
226
          'count', this.util.operationFactory.createTermExpression(object), false);
227

228
        const countProject = this.util.operationFactory.createProject(
12✔
229
          this.util.operationFactory.createExtend(
230
            this.util.operationFactory.createGroup(operation, [], [aggregator]), countOverVariable,
231
            this.util.operationFactory.createTermExpression(expressionVariable),
232
          ),
233
          [countOverVariable],
234
        );
235
        convertContext.terminalVariables.push(countOverVariable);
12✔
236

237
        // If no other selections exist (next to totalCount),
238
        // then we just return the count operations as-is,
239
        // otherwise, we join the count operation with all other selections
240
        if (!selections.length) {
12✔
241
          joinedOperation = countProject;
6✔
242
        } else {
243
          joinedOperation = this.util.operationFactory.createJoin([
6✔
244
            this.util.operationFactory.createProject(joinedOperation, []),
245
            countProject,
246
          ]);
247
        }
248
      }
249

250
      operation = joinedOperation;
306✔
251
    } else if (pushTerminalVariables && object.termType === 'Variable') {
432✔
252
      // If no nested selection sets exist,
253
      // consider the object variable as a terminal variable that should be selected.
254
      convertContext.terminalVariables.push(object);
378✔
255
    }
256

257
    // Wrap the operation in a slice if a 'first' or 'offset' argument was provided.
258
    if (offset || limit) {
738✔
259
      operation = this.util.operationFactory.createSlice(this.util.operationFactory.createProject(
6✔
260
        operation, algebraUtils.inScopeVariables(operation)), offset, limit);
261
    }
262

263
    // Override operation if needed
264
    return this.handleDirectiveOutputs(directiveOutputs, operation);
738✔
265
  }
266

267
  /**
268
   * Check if the given node is a meta field, for things like introspection.
269
   * If so, return a new operation for this, otherwise, null is returned.
270
   * @param {IConvertContext} convertContext A convert context.
271
   * @param {Term} subject The subject.
272
   * @param {string} fieldLabel The field label to convert.
273
   * @param {Pattern[]} auxiliaryPatterns Optional patterns that should be part of the BGP.
274
   * @return {Operation} An operation or undefined.
275
   */
276
  public handleMetaField(convertContext: IConvertContext, fieldLabel: string,
277
                         auxiliaryPatterns?: Algebra.Pattern[]): Algebra.Operation | undefined {
278
    // TODO: in the future, we should add support for GraphQL wide range of introspection features:
279
    // http://graphql.org/learn/introspection/
280
    if (fieldLabel === '__typename') {
690✔
281
      const object: RDF.Variable = this.util.nameToVariable(fieldLabel, convertContext);
6✔
282
      convertContext.terminalVariables.push(object);
6✔
283
      return this.util.operationFactory.createBgp([
6✔
284
        this.util.operationFactory.createPattern(
285
          convertContext.subject,
286
          this.util.dataFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
287
          this.util.nameToVariable(fieldLabel, convertContext),
288
          convertContext.graph,
289
        ),
290
      ].concat(auxiliaryPatterns || []));
6✔
291
    }
292
  }
293

294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc