@apollo/query-planner
Advanced tools
Comparing version 0.1.1 to 0.1.2
@@ -7,2 +7,4 @@ # CHANGELOG for `@apollo/query-planner` | ||
- This change is mostly a set of follow-up changes for PR #622. Most of these changes are internal (renaming, etc.). Some noteworthy changes worth mentioning are: the splitting of entity and value type metadata types and a conversion of GraphMap to an actual `Map` (which resulted in some additional assertions). [PR #656](https://github.com/apollographql/federation/pull/656) | ||
# v0.1.1 | ||
@@ -9,0 +11,0 @@ |
@@ -11,2 +11,17 @@ "use strict"; | ||
const composedSchema_1 = require("./composedSchema"); | ||
const debug_1 = require("./utilities/debug"); | ||
function stringIsTrue(str) { | ||
if (!str) { | ||
return false; | ||
} | ||
switch (str.toLocaleLowerCase()) { | ||
case "true": | ||
case "yes": | ||
case "1": | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
const debug = new debug_1.DebugLogger(stringIsTrue(process.env.APOLLO_QP_DEBUG)); | ||
const typenameField = { | ||
@@ -26,6 +41,13 @@ kind: graphql_1.Kind.FIELD, | ||
const isMutation = context.operation.operation === 'mutation'; | ||
debug.log(() => `Building plan for ${isMutation ? "mutation" : "query"} "${rootType}" (fragments: [${Object.keys(context.fragments)}], autoFragmentization: ${context.autoFragmentization})`); | ||
debug.group(`Collecting root fields:`); | ||
const fields = collectFields(context, context.newScope(rootType), context.operation.selectionSet); | ||
debug.groupEnd(`Collected root fields:`); | ||
debug.groupedValues(fields, FieldSet_1.debugPrintField); | ||
debug.group('Splitting root fields:'); | ||
const groups = isMutation | ||
? splitRootFieldsSerially(context, fields) | ||
: splitRootFields(context, fields); | ||
debug.groupEnd('Computed groups:'); | ||
debug.groupedValues(groups, debugPrintGroup); | ||
const nodes = groups.map(group => executionNodeForGroup(context, group, rootType)); | ||
@@ -280,2 +302,3 @@ return { | ||
for (const [parentType, fieldsForParentType] of FieldSet_1.groupByParentType(fieldsForResponseName)) { | ||
debug.group(() => FieldSet_1.debugPrintFields(fieldsForParentType)); | ||
const field = fieldsForParentType[0]; | ||
@@ -292,21 +315,32 @@ const { scope, fieldDef } = field; | ||
.map(type => type.name); | ||
if (roots.indexOf(parentType.name) > -1) | ||
if (roots.indexOf(parentType.name) > -1) { | ||
debug.groupEnd("Skipping __typename for root types"); | ||
continue; | ||
} | ||
} | ||
if (graphql_1.isIntrospectionType(graphql_1.getNamedType(fieldDef.type))) { | ||
debug.groupEnd(`Skipping introspection type ${fieldDef.type}`); | ||
continue; | ||
} | ||
if (graphql_1.isObjectType(parentType) && scope.possibleTypes.includes(parentType)) { | ||
debug.log(() => `${parentType} = object and ${parentType} ∈ [${scope.possibleTypes}]`); | ||
const group = groupForField(field); | ||
debug.log(() => `Initial fetch group for fields: ${debugPrintGroup(group)}`); | ||
group.fields.push(completeField(context, scope, group, path, fieldsForParentType)); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
} | ||
else { | ||
debug.log(() => `${parentType} ≠ object or ${parentType} ∉ [${scope.possibleTypes}]`); | ||
const possibleFieldDefs = scope.possibleTypes.map(runtimeType => context.getFieldDef(runtimeType, field.fieldNode)); | ||
const hasNoExtendingFieldDefs = !possibleFieldDefs.some((field) => { var _a; return (_a = composedSchema_1.getFederationMetadataForField(field)) === null || _a === void 0 ? void 0 : _a.graphName; }); | ||
if (hasNoExtendingFieldDefs) { | ||
debug.group(() => `No field of ${scope.possibleTypes} have federation directives, avoid type explosion.`); | ||
const group = groupForField(field); | ||
debug.groupEnd(() => `Initial fetch group for fields: ${debugPrintGroup(group)}`); | ||
group.fields.push(completeField(context, scope, group, path, fieldsForParentType)); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
continue; | ||
} | ||
const groupsByRuntimeParentTypes = new MultiMap_1.MultiMap(); | ||
debug.group('Computing fetch groups by runtime parent types'); | ||
for (const runtimeParentType of scope.possibleTypes) { | ||
@@ -320,4 +354,9 @@ const fieldDef = context.getFieldDef(runtimeParentType, field.fieldNode); | ||
} | ||
debug.groupEnd(`Fetch groups to resolvable runtime types:`); | ||
debug.groupedEntries(groupsByRuntimeParentTypes, debugPrintGroup, (v) => v.toString()); | ||
debug.group('Iterating on fetch groups'); | ||
for (const [group, runtimeParentTypes] of groupsByRuntimeParentTypes) { | ||
debug.group(() => `For initial fetch group ${debugPrintGroup(group)}:`); | ||
for (const runtimeParentType of runtimeParentTypes) { | ||
debug.group(`For runtime parent type ${runtimeParentType}:`); | ||
const fieldDef = context.getFieldDef(runtimeParentType, field.fieldNode); | ||
@@ -329,4 +368,8 @@ const fieldsWithRuntimeParentType = fieldsForParentType.map(field => ({ | ||
group.fields.push(completeField(context, context.newScope(runtimeParentType, scope), group, path, fieldsWithRuntimeParentType)); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
} | ||
debug.groupEnd(); | ||
} | ||
debug.groupEnd(); | ||
debug.groupEnd(); | ||
} | ||
@@ -355,3 +398,5 @@ } | ||
const subfields = collectSubfields(context, returnType, fields); | ||
debug.group(() => `Splitting collected sub-fields (${FieldSet_1.debugPrintFields(subfields)})`); | ||
splitSubfields(context, fieldPath, subfields, subGroup); | ||
debug.groupEnd(); | ||
parentGroup.otherDependentGroups.push(...subGroup.dependentGroups); | ||
@@ -505,2 +550,12 @@ let definition; | ||
} | ||
function debugPrintGroup(group) { | ||
let str = `Fetch(${group.serviceName}, ${FieldSet_1.debugPrintFields(group.fields)}`; | ||
if (group.dependentGroups.length !== 0) { | ||
str += `, deps: ${debugPrintGroups(group.dependentGroups)}`; | ||
} | ||
return str + ')'; | ||
} | ||
function debugPrintGroups(groups) { | ||
return '[' + groups.map(debugPrintGroup).join(', ') + ']'; | ||
} | ||
function buildOperationContext(schema, document, operationName) { | ||
@@ -593,4 +648,4 @@ let operation; | ||
getBaseService(parentType) { | ||
var _a; | ||
return (_a = composedSchema_1.getFederationMetadataForType(parentType)) === null || _a === void 0 ? void 0 : _a.graphName; | ||
const type = composedSchema_1.getFederationMetadataForType(parentType); | ||
return (type && composedSchema_1.isEntityTypeMetadata(type)) ? type.graphName : undefined; | ||
} | ||
@@ -602,3 +657,2 @@ getOwningService(parentType, fieldDef) { | ||
getKeyFields({ parentType, serviceName, fetchAll = false, }) { | ||
var _a, _b; | ||
const keyFields = []; | ||
@@ -614,3 +668,6 @@ keyFields.push({ | ||
for (const possibleType of this.getPossibleTypes(parentType)) { | ||
const keys = (_b = (_a = composedSchema_1.getFederationMetadataForType(possibleType)) === null || _a === void 0 ? void 0 : _a.keys) === null || _b === void 0 ? void 0 : _b.get(serviceName); | ||
const type = composedSchema_1.getFederationMetadataForType(possibleType); | ||
const keys = type && composedSchema_1.isEntityTypeMetadata(type) | ||
? type.keys.get(serviceName) | ||
: undefined; | ||
if (!(keys && keys.length > 0)) | ||
@@ -617,0 +674,0 @@ continue; |
@@ -8,4 +8,4 @@ "use strict"; | ||
const MultiMap_1 = require("../utilities/MultiMap"); | ||
const metadata_1 = require("./metadata"); | ||
function buildComposedSchema(document) { | ||
var _a, _b; | ||
const schema = graphql_1.buildASTSchema(document); | ||
@@ -36,3 +36,3 @@ const coreName = 'core'; | ||
assert_1.assert(graphql_1.isEnumType(graphEnumType), `${joinName}__Graph should be an enum`); | ||
const graphMap = Object.create(null); | ||
const graphMap = new Map(); | ||
schema.extensions = { | ||
@@ -50,6 +50,6 @@ ...schema.extensions, | ||
const url = graphDirectiveArgs['url']; | ||
graphMap[name] = { | ||
graphMap.set(name, { | ||
name: graphName, | ||
url, | ||
}; | ||
}); | ||
} | ||
@@ -63,11 +63,18 @@ for (const type of Object.values(schema.getTypeMap())) { | ||
const ownerDirectiveArgs = graphql_2.getArgumentValuesForDirective(ownerDirective, type.astNode); | ||
const typeMetadata = ownerDirectiveArgs | ||
? { | ||
graphName: graphMap[ownerDirectiveArgs === null || ownerDirectiveArgs === void 0 ? void 0 : ownerDirectiveArgs['graph']].name, | ||
let typeMetadata; | ||
if (ownerDirectiveArgs) { | ||
assert_1.assert(ownerDirectiveArgs.graph, `@${ownerDirective.name} directive requires a \`graph\` argument`); | ||
const graph = graphMap.get(ownerDirectiveArgs.graph); | ||
assertGraphFound(graph, ownerDirectiveArgs.graph, ownerDirective.name); | ||
typeMetadata = { | ||
graphName: graph.name, | ||
keys: new MultiMap_1.MultiMap(), | ||
isValueType: false, | ||
} | ||
: { | ||
}; | ||
} | ||
else { | ||
typeMetadata = { | ||
isValueType: true, | ||
}; | ||
} | ||
type.extensions = { | ||
@@ -78,8 +85,10 @@ ...type.extensions, | ||
const typeDirectivesArgs = graphql_2.getArgumentValuesForRepeatableDirective(typeDirective, type.astNode); | ||
assert_1.assert(!(typeMetadata.isValueType && typeDirectivesArgs.length >= 1), `GraphQL type "${type.name}" cannot have a @${typeDirective.name} \ | ||
assert_1.assert(metadata_1.isEntityTypeMetadata(typeMetadata) || typeDirectivesArgs.length === 0, `GraphQL type "${type.name}" cannot have a @${typeDirective.name} \ | ||
directive without an @${ownerDirective.name} directive`); | ||
for (const typeDirectiveArgs of typeDirectivesArgs) { | ||
const graphName = graphMap[typeDirectiveArgs['graph']].name; | ||
const keyFields = parseFieldSet(typeDirectiveArgs['key']); | ||
(_a = typeMetadata.keys) === null || _a === void 0 ? void 0 : _a.add(graphName, keyFields); | ||
assert_1.assert(typeDirectiveArgs.graph, `GraphQL type "${type.name}" must provide a \`graph\` argument to the @${typeDirective.name} directive`); | ||
const graph = graphMap.get(typeDirectiveArgs.graph); | ||
assertGraphFound(graph, typeDirectiveArgs.graph, typeDirective.name); | ||
const keyFields = graphql_2.parseFieldSet(typeDirectiveArgs['key']); | ||
typeMetadata.keys.add(graph.name, keyFields); | ||
} | ||
@@ -91,5 +100,11 @@ for (const fieldDef of Object.values(type.getFields())) { | ||
continue; | ||
const fieldMetadata = { | ||
graphName: (_b = graphMap[fieldDirectiveArgs === null || fieldDirectiveArgs === void 0 ? void 0 : fieldDirectiveArgs['graph']]) === null || _b === void 0 ? void 0 : _b.name, | ||
}; | ||
let fieldMetadata; | ||
if (fieldDirectiveArgs.graph) { | ||
const graph = graphMap.get(fieldDirectiveArgs.graph); | ||
assertGraphFound(graph, fieldDirectiveArgs.graph, fieldDirective.name); | ||
fieldMetadata = { graphName: graph.name }; | ||
} | ||
else { | ||
fieldMetadata = { graphName: undefined }; | ||
} | ||
fieldDef.extensions = { | ||
@@ -101,6 +116,6 @@ ...fieldDef.extensions, | ||
if (requires) { | ||
fieldMetadata.requires = parseFieldSet(requires); | ||
fieldMetadata.requires = graphql_2.parseFieldSet(requires); | ||
} | ||
if (provides) { | ||
fieldMetadata.provides = parseFieldSet(provides); | ||
fieldMetadata.provides = graphql_2.parseFieldSet(provides); | ||
} | ||
@@ -124,8 +139,5 @@ } | ||
exports.buildComposedSchema = buildComposedSchema; | ||
function parseFieldSet(source) { | ||
const selections = graphql_2.parseSelections(source); | ||
assert_1.assert(selections.every(graphql_2.isASTKind('Field', 'InlineFragment')), `Field sets may not contain fragment spreads, but found: "${source}"`); | ||
assert_1.assert(selections.length > 0, `Field sets may not be empty`); | ||
return selections; | ||
function assertGraphFound(graph, graphName, directiveName) { | ||
assert_1.assert(graph, `Programming error: found unexpected \`graph\` argument value "${graphName}" in @${directiveName} directive`); | ||
} | ||
//# sourceMappingURL=buildComposedSchema.js.map |
@@ -24,13 +24,16 @@ import { FieldNode, InlineFragmentNode, GraphQLField, GraphQLObjectType } from 'graphql'; | ||
} | ||
export declare type GraphMap = { | ||
[graphName: string]: Graph; | ||
}; | ||
export declare type GraphMap = Map<string, Graph>; | ||
export interface FederationSchemaMetadata { | ||
graphs: GraphMap; | ||
} | ||
export interface FederationTypeMetadata { | ||
graphName?: GraphName; | ||
keys?: MultiMap<GraphName, FieldSet>; | ||
isValueType: boolean; | ||
export declare type FederationTypeMetadata = FederationEntityTypeMetadata | FederationValueTypeMetadata; | ||
export interface FederationEntityTypeMetadata { | ||
graphName: GraphName; | ||
keys: MultiMap<GraphName, FieldSet>; | ||
isValueType: false; | ||
} | ||
interface FederationValueTypeMetadata { | ||
isValueType: true; | ||
} | ||
export declare function isEntityTypeMetadata(metadata: FederationTypeMetadata): metadata is FederationEntityTypeMetadata; | ||
export interface FederationFieldMetadata { | ||
@@ -41,2 +44,3 @@ graphName?: GraphName; | ||
} | ||
export {}; | ||
//# sourceMappingURL=metadata.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getFederationMetadataForField = exports.getFederationMetadataForType = void 0; | ||
exports.isEntityTypeMetadata = exports.getFederationMetadataForField = exports.getFederationMetadataForType = void 0; | ||
function getFederationMetadataForType(type) { | ||
@@ -14,2 +14,6 @@ var _a; | ||
exports.getFederationMetadataForField = getFederationMetadataForField; | ||
function isEntityTypeMetadata(metadata) { | ||
return !metadata.isValueType; | ||
} | ||
exports.isEntityTypeMetadata = isEntityTypeMetadata; | ||
//# sourceMappingURL=metadata.js.map |
@@ -7,2 +7,3 @@ import { FieldNode, GraphQLCompositeType, GraphQLField, SelectionSetNode, GraphQLObjectType, DirectiveNode } from 'graphql'; | ||
} | ||
export declare function debugPrintField(field: Field): string; | ||
export interface Scope<TParent extends GraphQLCompositeType> { | ||
@@ -14,4 +15,6 @@ parentType: TParent; | ||
} | ||
export declare function debugPrintScope<TParent extends GraphQLCompositeType>(scope: Scope<TParent>, deepDebug?: boolean): string; | ||
export declare type FieldSet = Field[]; | ||
export declare function printFields(fields?: FieldSet): string; | ||
export declare function debugPrintFields(fields?: FieldSet): string; | ||
export declare function matchesField(field: Field): (otherField: Field) => boolean; | ||
@@ -18,0 +21,0 @@ export declare const groupByResponseName: (iterable: Iterable<Field<GraphQLCompositeType>>) => Map<string, Field<GraphQLCompositeType>[]>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.selectionSetFromFieldSet = exports.groupByParentType = exports.groupByResponseName = exports.matchesField = exports.printFields = void 0; | ||
exports.selectionSetFromFieldSet = exports.groupByParentType = exports.groupByResponseName = exports.matchesField = exports.debugPrintFields = exports.printFields = exports.debugPrintScope = exports.debugPrintField = void 0; | ||
const graphql_1 = require("graphql"); | ||
const graphql_2 = require("./utilities/graphql"); | ||
const array_1 = require("./utilities/array"); | ||
function debugPrintField(field) { | ||
const def = field.fieldDef; | ||
return `(${def.name}: ${def.type})${debugPrintScope(field.scope)}`; | ||
} | ||
exports.debugPrintField = debugPrintField; | ||
function debugPrintScope(scope, deepDebug = false) { | ||
let enclosingStr = ''; | ||
if (scope.enclosingScope) { | ||
if (deepDebug) { | ||
enclosingStr = ' -> ' + debugPrintScope(scope.enclosingScope); | ||
} | ||
else { | ||
enclosingStr = ' ⋯'; | ||
} | ||
} | ||
return `<${scope.parentType} [${scope.possibleTypes}]${enclosingStr}>`; | ||
} | ||
exports.debugPrintScope = debugPrintScope; | ||
function printFields(fields) { | ||
@@ -17,2 +35,8 @@ if (!fields) | ||
exports.printFields = printFields; | ||
function debugPrintFields(fields) { | ||
if (!fields) | ||
return '[]'; | ||
return '[' + fields.map(debugPrintField).join(', ') + ']'; | ||
} | ||
exports.debugPrintFields = debugPrintFields; | ||
function matchesField(field) { | ||
@@ -19,0 +43,0 @@ return (otherField) => { |
@@ -1,2 +0,3 @@ | ||
import { ASTKindToNode, ASTNode, DirectiveNode, FieldNode, GraphQLCompositeType, GraphQLDirective, GraphQLField, GraphQLNullableType, GraphQLSchema, ListTypeNode, NamedTypeNode, SelectionNode, SelectionSetNode } from 'graphql'; | ||
import { ASTKindToNode, ASTNode, DirectiveNode, FieldNode, GraphQLCompositeType, GraphQLDirective, GraphQLField, GraphQLNullableType, GraphQLSchema, ListTypeNode, NamedTypeNode, SelectionNode } from 'graphql'; | ||
import { FieldSet } from '../composedSchema'; | ||
export declare function getFieldDef(schema: GraphQLSchema, parentType: GraphQLCompositeType, fieldName: string): GraphQLField<any, any> | undefined; | ||
@@ -6,5 +7,4 @@ export declare function getResponseName(node: FieldNode): string; | ||
export declare function astFromType(type: GraphQLNullableType): NamedTypeNode | ListTypeNode; | ||
export declare function printWithReducedWhitespace(ast: ASTNode): string; | ||
export declare function parseSelectionSet(source: string): SelectionSetNode; | ||
export declare function parseSelections(source: string): ReadonlyArray<SelectionNode>; | ||
export declare function parseFieldSet(source: string): FieldSet; | ||
export declare function getArgumentValuesForDirective(directiveDef: GraphQLDirective, node: { | ||
@@ -11,0 +11,0 @@ directives?: readonly DirectiveNode[]; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isASTKind = exports.getArgumentValuesForRepeatableDirective = exports.getArgumentValuesForDirective = exports.parseSelections = exports.parseSelectionSet = exports.printWithReducedWhitespace = exports.astFromType = exports.allNodesAreOfSameKind = exports.getResponseName = exports.getFieldDef = void 0; | ||
exports.isASTKind = exports.getArgumentValuesForRepeatableDirective = exports.getArgumentValuesForDirective = exports.parseFieldSet = exports.parseSelections = exports.astFromType = exports.allNodesAreOfSameKind = exports.getResponseName = exports.getFieldDef = void 0; | ||
const graphql_1 = require("graphql"); | ||
@@ -52,18 +52,24 @@ const values_1 = require("graphql/execution/values"); | ||
exports.astFromType = astFromType; | ||
function printWithReducedWhitespace(ast) { | ||
return graphql_1.print(ast) | ||
.replace(/\s+/g, ' ') | ||
.trim(); | ||
} | ||
exports.printWithReducedWhitespace = printWithReducedWhitespace; | ||
function parseSelectionSet(source) { | ||
return graphql_1.parse(`query ${source}`) | ||
.definitions[0].selectionSet; | ||
} | ||
exports.parseSelectionSet = parseSelectionSet; | ||
function parseSelections(source) { | ||
return graphql_1.parse(`query { ${source} }`) | ||
.definitions[0].selectionSet.selections; | ||
const parsed = graphql_1.parse(`{${source}}`); | ||
assert_1.assert(parsed.definitions.length === 1, `Invalid FieldSet provided: '${source}'. FieldSets may not contain operations within them.`); | ||
return parsed.definitions[0].selectionSet | ||
.selections; | ||
} | ||
exports.parseSelections = parseSelections; | ||
function parseFieldSet(source) { | ||
const selections = parseSelections(source); | ||
const selectionSetNode = { | ||
kind: graphql_1.Kind.SELECTION_SET, | ||
selections, | ||
}; | ||
graphql_1.visit(selectionSetNode, { | ||
FragmentSpread() { | ||
throw Error(`Field sets may not contain fragment spreads, but found: "${source}"`); | ||
}, | ||
}); | ||
assert_1.assert(selections.length > 0, `Field sets may not be empty`); | ||
return selections; | ||
} | ||
exports.parseFieldSet = parseFieldSet; | ||
function getArgumentValuesForDirective(directiveDef, node) { | ||
@@ -70,0 +76,0 @@ assert_1.assert(!directiveDef.isRepeatable, 'Use getArgumentValuesForRepeatableDirective for repeatable directives'); |
{ | ||
"name": "@apollo/query-planner", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "Apollo Query Planner", | ||
@@ -28,2 +28,3 @@ "author": "Apollo <opensource@apollographql.com>", | ||
"dependencies": { | ||
"chalk": "^4.1.0", | ||
"pretty-format": "^26.0.0" | ||
@@ -34,3 +35,3 @@ }, | ||
}, | ||
"gitHead": "695070a526f8ddd1eb4d00044a08b009dd2ecc2b" | ||
"gitHead": "a69e377cdd5bc56f350bac227f3cc84ee5f9d777" | ||
} |
@@ -41,2 +41,4 @@ import { isNotNullOrUndefined } from './utilities/predicates'; | ||
Scope, | ||
debugPrintField, | ||
debugPrintFields, | ||
} from './FieldSet'; | ||
@@ -54,4 +56,26 @@ import { | ||
import { MultiMap } from './utilities/MultiMap'; | ||
import { getFederationMetadataForType, getFederationMetadataForField } from './composedSchema'; | ||
import { | ||
getFederationMetadataForType, | ||
getFederationMetadataForField, | ||
isEntityTypeMetadata, | ||
} from './composedSchema'; | ||
import { DebugLogger } from './utilities/debug'; | ||
function stringIsTrue(str?: string) : boolean { | ||
if (!str) { | ||
return false; | ||
} | ||
switch (str.toLocaleLowerCase()) { | ||
case "true": | ||
case "yes": | ||
case "1": | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
const debug = new DebugLogger(stringIsTrue(process.env.APOLLO_QP_DEBUG)); | ||
const typenameField = { | ||
@@ -94,2 +118,5 @@ kind: Kind.FIELD, | ||
debug.log(() => `Building plan for ${isMutation ? "mutation" : "query"} "${rootType}" (fragments: [${Object.keys(context.fragments)}], autoFragmentization: ${context.autoFragmentization})`); | ||
debug.group(`Collecting root fields:`); | ||
const fields = collectFields( | ||
@@ -100,3 +127,6 @@ context, | ||
); | ||
debug.groupEnd(`Collected root fields:`); | ||
debug.groupedValues(fields, debugPrintField); | ||
debug.group('Splitting root fields:'); | ||
// Mutations are a bit more specific in how FetchGroups can be built, as some | ||
@@ -107,2 +137,4 @@ // calls to the same service may need to be executed serially. | ||
: splitRootFields(context, fields); | ||
debug.groupEnd('Computed groups:'); | ||
debug.groupedValues(groups, debugPrintGroup); | ||
@@ -412,2 +444,8 @@ const nodes = groups.map(group => | ||
// Committed by @trevor-scheer but authored by @martijnwalraven | ||
// Treat abstract types as value types to replicate type explosion fix | ||
// XXX: this replicates the behavior of the Rust query planner implementation, | ||
// in order to get the tests passing before making further changes. But the | ||
// type explosion fix this depends on is fundamentally flawed and needs to | ||
// be replaced. | ||
const parentIsValueType = !isObjectType(parentType) || getFederationMetadataForType(parentType)?.isValueType; | ||
@@ -539,2 +577,4 @@ | ||
debug.group(() => debugPrintFields(fieldsForParentType)); | ||
const field = fieldsForParentType[0]; | ||
@@ -553,3 +593,6 @@ const { scope, fieldDef } = field; | ||
.map(type => type.name); | ||
if (roots.indexOf(parentType.name) > -1) continue; | ||
if (roots.indexOf(parentType.name) > -1) { | ||
debug.groupEnd("Skipping __typename for root types"); | ||
continue; | ||
} | ||
} | ||
@@ -559,2 +602,3 @@ | ||
if (isIntrospectionType(getNamedType(fieldDef.type))) { | ||
debug.groupEnd(`Skipping introspection type ${fieldDef.type}`); | ||
continue; | ||
@@ -566,3 +610,5 @@ } | ||
// group. | ||
debug.log(() => `${parentType} = object and ${parentType} ∈ [${scope.possibleTypes}]`); | ||
const group = groupForField(field as Field<GraphQLObjectType>); | ||
debug.log(() => `Initial fetch group for fields: ${debugPrintGroup(group)}`); | ||
group.fields.push( | ||
@@ -577,3 +623,5 @@ completeField( | ||
); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
} else { | ||
debug.log(() => `${parentType} ≠ object or ${parentType} ∉ [${scope.possibleTypes}]`); | ||
// For interfaces however, we need to look at all possible runtime types. | ||
@@ -601,6 +649,9 @@ | ||
if (hasNoExtendingFieldDefs) { | ||
debug.group(() => `No field of ${scope.possibleTypes} have federation directives, avoid type explosion.`); | ||
const group = groupForField(field as Field<GraphQLObjectType>); | ||
debug.groupEnd(() => `Initial fetch group for fields: ${debugPrintGroup(group)}`); | ||
group.fields.push( | ||
completeField(context, scope, group, path, fieldsForParentType) | ||
); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
continue; | ||
@@ -616,2 +667,3 @@ } | ||
debug.group('Computing fetch groups by runtime parent types'); | ||
for (const runtimeParentType of scope.possibleTypes) { | ||
@@ -631,10 +683,16 @@ const fieldDef = context.getFieldDef( | ||
} | ||
debug.groupEnd(`Fetch groups to resolvable runtime types:`); | ||
debug.groupedEntries(groupsByRuntimeParentTypes, debugPrintGroup, (v) => v.toString()); | ||
debug.group('Iterating on fetch groups'); | ||
// We add the field separately for each runtime parent type. | ||
for (const [group, runtimeParentTypes] of groupsByRuntimeParentTypes) { | ||
debug.group(() => `For initial fetch group ${debugPrintGroup(group)}:`); | ||
for (const runtimeParentType of runtimeParentTypes) { | ||
// We need to adjust the fields to contain the right fieldDef for | ||
// their runtime parent type. | ||
debug.group(`For runtime parent type ${runtimeParentType}:`); | ||
const fieldDef = context.getFieldDef( | ||
runtimeParentType, | ||
@@ -658,4 +716,9 @@ field.fieldNode, | ||
); | ||
debug.groupEnd(() => `Updated fetch group: ${debugPrintGroup(group)}`); | ||
} | ||
debug.groupEnd(); | ||
} | ||
debug.groupEnd(); // Group started before the immediate for loop | ||
debug.groupEnd(); // Group started at the beginning of this 'top-level' iteration. | ||
} | ||
@@ -703,3 +766,5 @@ } | ||
const subfields = collectSubfields(context, returnType, fields); | ||
debug.group(() => `Splitting collected sub-fields (${debugPrintFields(subfields)})`); | ||
splitSubfields(context, fieldPath, subfields, subGroup); | ||
debug.groupEnd(); | ||
@@ -806,2 +871,17 @@ parentGroup.otherDependentGroups.push(...subGroup.dependentGroups); | ||
// Committed by @trevor-scheer but authored by @martijnwalraven | ||
// This replicates the behavior added to the Rust query planner in #178. | ||
// Unfortunately, the logic in there is seriously flawed, and may lead to | ||
// unexpected results. The assumption seems to be that fields with the | ||
// same parent type are always nested in the same inline fragment. That | ||
// is not necessarily true however, and because we take the directives | ||
// from the scope of the first field with a particular parent type, | ||
// those directives will be applied to all other fields that have the | ||
// same parent type even if the directives aren't meant to apply to them | ||
// because they were nested in a different inline fragment. (That also | ||
// means that if the scope of the first field doesn't have directives, | ||
// directives that would have applied to other fields will be lost.) | ||
// Note that this also applies to `@skip` and `@include`, which could | ||
// lead to invalid query plans that fail at runtime because expected | ||
// fields are missing from a subgraph response. | ||
newScope.directives = selection.directives; | ||
@@ -937,2 +1017,16 @@ | ||
// Provides a string representation of a `FetchGroup` suitable for debugging. | ||
function debugPrintGroup(group: FetchGroup): string { | ||
let str = `Fetch(${group.serviceName}, ${debugPrintFields(group.fields)}`; | ||
if (group.dependentGroups.length !== 0) { | ||
str += `, deps: ${debugPrintGroups(group.dependentGroups)}` | ||
} | ||
return str + ')'; | ||
} | ||
// Provides a string representation of an array of `FetchGroup` suitable for debugging. | ||
function debugPrintGroups(groups: FetchGroup[]): string { | ||
return '[' + groups.map(debugPrintGroup).join(', ') + ']' | ||
} | ||
// Adapted from buildExecutionContext in graphql-js | ||
@@ -1088,3 +1182,4 @@ export function buildOperationContext( | ||
getBaseService(parentType: GraphQLObjectType): string | undefined { | ||
return getFederationMetadataForType(parentType)?.graphName; | ||
const type = getFederationMetadataForType(parentType); | ||
return (type && isEntityTypeMetadata(type)) ? type.graphName : undefined; | ||
} | ||
@@ -1123,3 +1218,7 @@ | ||
for (const possibleType of this.getPossibleTypes(parentType)) { | ||
const keys = getFederationMetadataForType(possibleType)?.keys?.get(serviceName); | ||
const type = getFederationMetadataForType(possibleType); | ||
const keys = | ||
type && isEntityTypeMetadata(type) | ||
? type.keys.get(serviceName) | ||
: undefined; | ||
@@ -1126,0 +1225,0 @@ if (!(keys && keys.length > 0)) continue; |
@@ -17,4 +17,3 @@ import { | ||
getArgumentValuesForRepeatableDirective, | ||
isASTKind, | ||
parseSelections, | ||
parseFieldSet, | ||
} from '../utilities/graphql'; | ||
@@ -25,4 +24,6 @@ import { MultiMap } from '../utilities/MultiMap'; | ||
FederationTypeMetadata, | ||
FieldSet, | ||
FederationEntityTypeMetadata, | ||
GraphMap, | ||
isEntityTypeMetadata, | ||
Graph, | ||
} from './metadata'; | ||
@@ -87,3 +88,3 @@ | ||
const graphMap: GraphMap = Object.create(null); | ||
const graphMap: GraphMap = new Map(); | ||
@@ -112,6 +113,6 @@ schema.extensions = { | ||
graphMap[name] = { | ||
graphMap.set(name, { | ||
name: graphName, | ||
url, | ||
}; | ||
}); | ||
} | ||
@@ -135,12 +136,22 @@ | ||
const typeMetadata: FederationTypeMetadata = ownerDirectiveArgs | ||
? { | ||
graphName: graphMap[ownerDirectiveArgs?.['graph']].name, | ||
keys: new MultiMap(), | ||
isValueType: false, | ||
} | ||
: { | ||
isValueType: true, | ||
}; | ||
let typeMetadata: FederationTypeMetadata; | ||
if (ownerDirectiveArgs) { | ||
assert( | ||
ownerDirectiveArgs.graph, | ||
`@${ownerDirective.name} directive requires a \`graph\` argument`, | ||
); | ||
const graph = graphMap.get(ownerDirectiveArgs.graph); | ||
assertGraphFound(graph, ownerDirectiveArgs.graph, ownerDirective.name); | ||
typeMetadata = { | ||
graphName: graph.name, | ||
keys: new MultiMap(), | ||
isValueType: false, | ||
}; | ||
} else { | ||
typeMetadata = { | ||
isValueType: true, | ||
}; | ||
} | ||
type.extensions = { | ||
@@ -156,4 +167,7 @@ ...type.extensions, | ||
// The assertion here guarantees the safety of the type cast below | ||
// (typeMetadata as FederationEntityTypeMetadata). Adjustments to this assertion | ||
// should account for this dependency. | ||
assert( | ||
!(typeMetadata.isValueType && typeDirectivesArgs.length >= 1), | ||
isEntityTypeMetadata(typeMetadata) || typeDirectivesArgs.length === 0, | ||
`GraphQL type "${type.name}" cannot have a @${typeDirective.name} \ | ||
@@ -164,7 +178,17 @@ directive without an @${ownerDirective.name} directive`, | ||
for (const typeDirectiveArgs of typeDirectivesArgs) { | ||
const graphName = graphMap[typeDirectiveArgs['graph']].name; | ||
assert( | ||
typeDirectiveArgs.graph, | ||
`GraphQL type "${type.name}" must provide a \`graph\` argument to the @${typeDirective.name} directive`, | ||
); | ||
const graph = graphMap.get(typeDirectiveArgs.graph); | ||
assertGraphFound(graph, typeDirectiveArgs.graph, typeDirective.name); | ||
const keyFields = parseFieldSet(typeDirectiveArgs['key']); | ||
typeMetadata.keys?.add(graphName, keyFields); | ||
// We know we won't actually be looping here in the case of a value type | ||
// based on the assertion above, but TS is not able to infer that. | ||
(typeMetadata as FederationEntityTypeMetadata).keys.add( | ||
graph.name, | ||
keyFields, | ||
); | ||
} | ||
@@ -185,5 +209,11 @@ | ||
const fieldMetadata: FederationFieldMetadata = { | ||
graphName: graphMap[fieldDirectiveArgs?.['graph']]?.name, | ||
}; | ||
let fieldMetadata: FederationFieldMetadata; | ||
if (fieldDirectiveArgs.graph) { | ||
const graph = graphMap.get(fieldDirectiveArgs.graph); | ||
// This should never happen, but the assertion guarantees the existence of `graph` | ||
assertGraphFound(graph, fieldDirectiveArgs.graph, fieldDirective.name); | ||
fieldMetadata = { graphName: graph.name }; | ||
} else { | ||
fieldMetadata = { graphName: undefined }; | ||
} | ||
@@ -237,13 +267,9 @@ fieldDef.extensions = { | ||
function parseFieldSet(source: string): FieldSet { | ||
const selections = parseSelections(source); | ||
// This should never happen, hence 'programming error', but this assertion | ||
// guarantees the existence of `graph`. | ||
function assertGraphFound(graph: Graph | undefined, graphName: string, directiveName: string): asserts graph { | ||
assert( | ||
selections.every(isASTKind('Field', 'InlineFragment')), | ||
`Field sets may not contain fragment spreads, but found: "${source}"`, | ||
graph, | ||
`Programming error: found unexpected \`graph\` argument value "${graphName}" in @${directiveName} directive`, | ||
); | ||
assert(selections.length > 0, `Field sets may not be empty`); | ||
return selections; | ||
} |
@@ -35,2 +35,7 @@ import { FieldNode, InlineFragmentNode, GraphQLField, GraphQLObjectType } from 'graphql'; | ||
export type GraphName = string; | ||
// Without rewriting a number of AST types from graphql-js, this typing is | ||
// technically too relaxed. Recursive selections are not excluded from containing | ||
// FragmentSpreads, which is what this type is aiming to achieve (and accomplishes | ||
// at the root level, but not recursively) | ||
export type FieldSet = readonly (FieldNode | InlineFragmentNode)[]; | ||
@@ -43,12 +48,27 @@ | ||
export type GraphMap = { [graphName: string]: Graph }; | ||
export type GraphMap = Map<string, Graph>; | ||
export interface FederationSchemaMetadata { | ||
graphs: GraphMap; | ||
} | ||
export interface FederationTypeMetadata { | ||
graphName?: GraphName; | ||
keys?: MultiMap<GraphName, FieldSet>; | ||
isValueType: boolean; | ||
export type FederationTypeMetadata = | ||
| FederationEntityTypeMetadata | ||
| FederationValueTypeMetadata; | ||
export interface FederationEntityTypeMetadata { | ||
graphName: GraphName; | ||
keys: MultiMap<GraphName, FieldSet>, | ||
isValueType: false; | ||
} | ||
interface FederationValueTypeMetadata { | ||
isValueType: true; | ||
} | ||
export function isEntityTypeMetadata( | ||
metadata: FederationTypeMetadata, | ||
): metadata is FederationEntityTypeMetadata { | ||
return !metadata.isValueType; | ||
} | ||
export interface FederationFieldMetadata { | ||
@@ -55,0 +75,0 @@ graphName?: GraphName; |
@@ -24,2 +24,16 @@ import { | ||
/** | ||
* Provides a string representation of a field suitable for debugging. | ||
* | ||
* The format looks like '(a: Int)<A [A1, A2]>' where 'a' is the field name, 'Int' is its type, and '<...>' is its | ||
* scope (@see debugScope for details). | ||
* | ||
* @param field - the field object to convert. | ||
* @return a string representation of the field. | ||
*/ | ||
export function debugPrintField(field: Field) : string { | ||
const def = field.fieldDef; | ||
return `(${def.name}: ${def.type})${debugPrintScope(field.scope)}`; | ||
} | ||
export interface Scope<TParent extends GraphQLCompositeType> { | ||
@@ -32,2 +46,24 @@ parentType: TParent; | ||
/** | ||
* Provides a string representation of a field suitable for debugging. | ||
* | ||
* The format looks like '<A [A1, A2]>' where 'A' is the scope 'parentType' and '[A1, A2]' are the 'possibleTypes'. | ||
* | ||
* @param scope - the scope object to convert. | ||
* @param deepDebug - whether to also display enclosed scopes. | ||
* @return a string representation of the scope. | ||
*/ | ||
export function debugPrintScope<TParent extends GraphQLCompositeType>( | ||
scope: Scope<TParent>, deepDebug: boolean = false) : string { | ||
let enclosingStr = ''; | ||
if (scope.enclosingScope) { | ||
if (deepDebug) { | ||
enclosingStr = ' -> ' + debugPrintScope(scope.enclosingScope); | ||
} else { | ||
enclosingStr = ' ⋯'; // show an elipsis so we know there is an enclosing scope, but it's just not displayed. | ||
} | ||
} | ||
return`<${scope.parentType} [${scope.possibleTypes}]${enclosingStr}>`; | ||
} | ||
export type FieldSet = Field[]; | ||
@@ -46,2 +82,15 @@ | ||
/** | ||
* Provides a string representation of a field set for debugging. | ||
* | ||
* The format is the list of fields as displayed by `debugField` within square brackets. | ||
* | ||
* @param fields - the field set object to convert. | ||
* @return a string representation of the field. | ||
*/ | ||
export function debugPrintFields(fields?: FieldSet) : string { | ||
if (!fields) return '[]'; | ||
return '[' + fields.map(debugPrintField).join(', ') + ']' | ||
} | ||
export function matchesField(field: Field) { | ||
@@ -157,2 +206,6 @@ // TODO: Compare parent type and arguments | ||
// Committed by @trevor-scheer but authored by @martijnwalraven | ||
// XXX: This code has more problems and should be replaced by proper recursive | ||
// selection set merging, but removing the unnecessary distinction between | ||
// aliased fields and non-aliased fields at least fixes the test. | ||
const mergedFieldNodes = Array.from( | ||
@@ -159,0 +212,0 @@ groupBy((node: FieldNode) => node.alias?.value ?? node.name.value)( |
@@ -0,1 +1,10 @@ | ||
/** | ||
* For lack of a "home of federation utilities", this function is copy/pasted | ||
* verbatim across the federation, gateway, and query-planner packages. Any changes | ||
* made here should be reflected in the other two locations as well. | ||
* | ||
* @param condition | ||
* @param message | ||
* @throws | ||
*/ | ||
export function assert(condition: any, message: string): asserts condition { | ||
@@ -2,0 +11,0 @@ if (!condition) { |
@@ -15,2 +15,3 @@ import { | ||
GraphQLUnionType, | ||
InlineFragmentNode, | ||
isListType, | ||
@@ -23,11 +24,11 @@ isNonNullType, | ||
parse, | ||
print, | ||
SchemaMetaFieldDef, | ||
SelectionNode, | ||
SelectionSetNode, | ||
TypeMetaFieldDef, | ||
TypeNameMetaFieldDef, | ||
TypeNode, | ||
visit, | ||
} from 'graphql'; | ||
import { getArgumentValues } from 'graphql/execution/values'; | ||
import { FieldSet } from '../composedSchema'; | ||
import { assert } from './assert'; | ||
@@ -102,18 +103,47 @@ | ||
export function printWithReducedWhitespace(ast: ASTNode): string { | ||
return print(ast) | ||
.replace(/\s+/g, ' ') | ||
.trim(); | ||
/** | ||
* For lack of a "home of federation utilities", this function is copy/pasted | ||
* verbatim across the federation, gateway, and query-planner packages. Any changes | ||
* made here should be reflected in the other two locations as well. | ||
* | ||
* @param source A string representing a FieldSet | ||
* @returns A parsed FieldSet | ||
*/ | ||
export function parseSelections(source: string): ReadonlyArray<SelectionNode> { | ||
const parsed = parse(`{${source}}`); | ||
assert( | ||
parsed.definitions.length === 1, | ||
`Invalid FieldSet provided: '${source}'. FieldSets may not contain operations within them.`, | ||
); | ||
return (parsed.definitions[0] as OperationDefinitionNode).selectionSet | ||
.selections; | ||
} | ||
export function parseSelectionSet(source: string): SelectionSetNode { | ||
return (parse(`query ${source}`) | ||
.definitions[0] as OperationDefinitionNode).selectionSet; | ||
} | ||
// TODO: should we be using this everywhere we're using `parseSelections`? | ||
export function parseFieldSet(source: string): FieldSet { | ||
const selections = parseSelections(source); | ||
export function parseSelections(source: string): ReadonlyArray<SelectionNode> { | ||
return (parse(`query { ${source} }`) | ||
.definitions[0] as OperationDefinitionNode).selectionSet.selections; | ||
const selectionSetNode = { | ||
kind: Kind.SELECTION_SET, | ||
selections, | ||
}; | ||
visit(selectionSetNode, { | ||
FragmentSpread() { | ||
throw Error( | ||
`Field sets may not contain fragment spreads, but found: "${source}"`, | ||
); | ||
}, | ||
}); | ||
// I'm not sure this case is possible - an empty string will first throw a | ||
// graphql syntax error. Can you get 0 selections any other way? | ||
assert(selections.length > 0, `Field sets may not be empty`); | ||
// This cast is asserted above by the visitor, ensuring that both `selections` | ||
// and any recursive `selections` are not `FragmentSpreadNode`s | ||
return selections as readonly (FieldNode | InlineFragmentNode)[]; | ||
} | ||
// Using `getArgumentValues` from `graphql-js` ensures that arguments are of the right type, | ||
@@ -120,0 +150,0 @@ // and that required arguments are present. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
373032
120
4498
3
1
+ Addedchalk@^4.1.0