UNPKG

@theguild/federation-composition

Version:
299 lines (298 loc) 13.1 kB
import { isTypeDefinitionNode, Kind, parse, specifiedScalarTypes, } from 'graphql'; import { createSpecSchema } from '../../specifications/federation.js'; import { sdl as linkSpecSdl } from '../../specifications/link.js'; import { stripTypeModifiers } from '../../utils/state.js'; import { satisfiesVersionRange } from '../../utils/version.js'; import { TypeKind } from '../state.js'; const linkSpec = parse(linkSpecSdl); const linkSpecDirectives = linkSpec.definitions.filter((def) => def.kind === Kind.DIRECTIVE_DEFINITION); const linkSpecTypes = linkSpec.definitions.filter(isTypeDefinitionNode); export function createSimpleValidationContext(typeDefs, typeNodeInfo) { let reportedErrors = []; const directiveDefinitionMap = new Map(); const typeDefinitionMap = new Map(); for (const definition of typeDefs.definitions) { if (definition.kind === Kind.DIRECTIVE_DEFINITION) { directiveDefinitionMap.set(definition.name.value, definition); } else if ('name' in definition && definition.name && definition.name.kind === Kind.NAME) { typeDefinitionMap.set(definition.name.value, { name: definition.name, kind: definition.kind, }); } } return { getDocument() { return typeDefs; }, getKnownDirectiveDefinition(name) { return directiveDefinitionMap.get(name); }, getKnownTypeDefinition(name) { return typeDefinitionMap.get(name); }, getSchemaCoordinate(ancestors) { let coordinate = ''; for (let i = 0; i < ancestors.length; i++) { const ancestor = ancestors[i]; if ('kind' in ancestor && ancestor.kind !== Kind.DOCUMENT) { const name = ancestor.kind === Kind.SCHEMA_DEFINITION || ancestor.kind === Kind.SCHEMA_EXTENSION ? 'schema' : 'name' in ancestor && ancestor.name ? ancestor.name.value : ''; if (coordinate.length > 0) { coordinate = coordinate + '.' + name; } else { coordinate = name; } } } return coordinate; }, reportError(error) { reportedErrors.push(error); }, collectReportedErrors() { const errors = reportedErrors; reportedErrors = []; return errors; }, }; } export function createSubgraphValidationContext(subgraph, federation, typeNodeInfo, stateBuilder) { const { version, imports } = federation; const availableSpec = createSpecSchema(version, imports); const knownSpec = createSpecSchema(version); const knownSubgraphEntities = new Map(); for (const def of subgraph.typeDefs.definitions.filter(def => def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.OBJECT_TYPE_EXTENSION || def.kind === Kind.INTERFACE_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_EXTENSION)) { const found = knownSubgraphEntities.get(def.name.value); if (!found) { knownSubgraphEntities.set(def.name.value, { ...def }); continue; } found.fields = (found.fields ?? []).concat(def.fields ?? []); found.interfaces = (found.interfaces ?? []).concat(def.interfaces ?? []); found.directives = (found.directives ?? []).concat(def.directives ?? []); if (def.kind === Kind.OBJECT_TYPE_DEFINITION && found.kind === Kind.OBJECT_TYPE_EXTENSION) { found.kind = Kind.OBJECT_TYPE_DEFINITION; } else if (def.kind === Kind.INTERFACE_TYPE_DEFINITION && found.kind === Kind.INTERFACE_TYPE_EXTENSION) { found.kind = Kind.INTERFACE_TYPE_DEFINITION; } } const knownSubgraphDirectiveDefinitions = new Map(subgraph.typeDefs.definitions.filter(def => def.kind === Kind.DIRECTIVE_DEFINITION).map(def => [def.name.value, def])); const leafTypeNames = new Set(specifiedScalarTypes.map(type => type.name)); for (const def of subgraph.typeDefs.definitions) { if (def.kind === Kind.SCALAR_TYPE_DEFINITION || def.kind === Kind.SCALAR_TYPE_EXTENSION || def.kind === Kind.ENUM_TYPE_DEFINITION || def.kind === Kind.ENUM_TYPE_EXTENSION) { leafTypeNames.add(def.name.value); } } let reportedErrors = []; const markedAsExternal = new Set(); const markedAsUsed = new Set(); const markedAsKeyField = new Set(); const overwrittenFederationDefinitionNames = new Set(); const directiveAlternativeNamesMap = new Map(); for (const specDirective of availableSpec.directives) { const isFederationPrefixed = specDirective.name.value.startsWith('federation__'); if (isFederationPrefixed) { const normalizedName = specDirective.name.value.replace('federation__', ''); const setOfNames = directiveAlternativeNamesMap.get(normalizedName); if (!setOfNames) { directiveAlternativeNamesMap.set(normalizedName, new Set([specDirective.name.value])); } } else { const { alias } = imports.find(i => i.name.replace(/^@/, '') === specDirective.name.value) ?? { alias: undefined, }; let setOfNames = directiveAlternativeNamesMap.get(specDirective.name.value); if (!setOfNames) { directiveAlternativeNamesMap.set(specDirective.name.value, new Set()); setOfNames = directiveAlternativeNamesMap.get(specDirective.name.value); } setOfNames.add(alias ? alias.replace(/^@/, '') : specDirective.name.value); setOfNames.add(`federation__${specDirective.name.value}`); } } const typeAlternativeNamesMap = new Map(); for (const specType of availableSpec.types) { const isFederationPrefixed = specType.name.value.startsWith('federation__'); if (isFederationPrefixed) { const normalizedName = specType.name.value.replace('federation__', ''); const setOfNames = typeAlternativeNamesMap.get(normalizedName); if (!setOfNames) { typeAlternativeNamesMap.set(normalizedName, new Set([specType.name.value])); } } else { const { alias } = imports.find(i => i.name === specType.name.value) ?? { alias: undefined, }; let setOfNames = typeAlternativeNamesMap.get(specType.name.value); if (!setOfNames) { typeAlternativeNamesMap.set(specType.name.value, new Set()); setOfNames = typeAlternativeNamesMap.get(specType.name.value); } setOfNames.add(alias ? alias : specType.name.value); setOfNames.add(`federation__${specType.name.value}`); } } const importedTypesSet = new Set(availableSpec.types.map(t => t.name.value)); if (importedTypesSet.size) { subgraph.typeDefs.definitions.forEach(def => { if ('name' in def && def.name && importedTypesSet.has(def.name.value)) { overwrittenFederationDefinitionNames.add(def.name.value); } }); } return { stateBuilder, federationImports: imports, isLinkSpecDirective(name) { return linkSpecDirectives.some(d => d.name.value === name); }, isLinkSpecType(name) { return linkSpecTypes.some(t => t.name.value === name); }, isAvailableFederationType(name) { const alternativeNames = typeAlternativeNamesMap.get(name); if (alternativeNames) { return alternativeNames.has(name); } return false; }, isAvailableFederationDirective(specDirectiveName, directiveNode) { const alternativeNames = directiveAlternativeNamesMap.get(specDirectiveName); if (alternativeNames) { return alternativeNames.has(typeof directiveNode.name === 'string' ? directiveNode.name : directiveNode.name.value); } return false; }, satisfiesVersionRange(range) { return satisfiesVersionRange(version, range); }, getKnownFederationDirectives() { return knownSpec.directives; }, getAvailableFederationDirectives() { return availableSpec.directives; }, isLeafType(typeName) { return leafTypeNames.has(typeName); }, getSubgraphObjectOrInterfaceTypes() { return knownSubgraphEntities; }, getSubgraphDirectiveDefinitions() { return knownSubgraphDirectiveDefinitions; }, getAvailableFederationTypeAndDirectiveDefinitions() { return [].concat(availableSpec.directives.map(d => { const alias = imports.find(i => i.name.replace(/^@/, '') === d.name.value)?.alias; if (alias) { d.name.value = alias.replace(/^@/, ''); } return d; }), availableSpec.types.map(t => { const alias = imports.find(i => i.name === t.name.value)?.alias; if (alias) { t.name.value = alias; } return t; })); }, typeNodeInfo, getDocument() { return subgraph.typeDefs; }, getSubgraphName() { return subgraph.name; }, getSubgraphId() { return subgraph.id; }, markAsExternal(coordinate) { markedAsExternal.add(coordinate); }, markAsUsed(reason, kind, typeName, fieldName) { if (!fieldName.startsWith('__') && !typeName.startsWith('__') && reason === 'fields') { switch (kind) { case Kind.OBJECT_TYPE_DEFINITION: case Kind.OBJECT_TYPE_EXTENSION: { stateBuilder.objectType.field.setUsed(typeName, fieldName); break; } case Kind.INTERFACE_TYPE_DEFINITION: case Kind.INTERFACE_TYPE_EXTENSION: { stateBuilder.interfaceType.field.setUsed(typeName, fieldName); break; } } } markedAsUsed.add(`${typeName}.${fieldName}`); }, markAsKeyField(coordinate) { markedAsKeyField.add(coordinate); }, markAsFederationDefinitionReplacement(name) { overwrittenFederationDefinitionNames.add(name); }, collectFederationDefinitionReplacements() { return overwrittenFederationDefinitionNames; }, collectUnusedExternal() { if (version === 'v1.0') { return Array.from(markedAsExternal).filter(c => !markedAsUsed.has(c) && markedAsKeyField.has(c)); } const unused = Array.from(markedAsExternal).filter(c => !markedAsUsed.has(c)); return unused.filter(coordinate => { const [typeName, fieldName] = coordinate.split('.'); const typeDef = stateBuilder.state.types.get(typeName); if (!typeDef) { return true; } if (typeDef.kind === TypeKind.OBJECT && !stateBuilder.isInterfaceObject(typeName)) { const fieldDef = typeDef.fields.get(fieldName); if (fieldDef) { const outputTypeName = stripTypeModifiers(fieldDef.type); const outputType = stateBuilder.state.types.get(outputTypeName); if (outputType?.kind === TypeKind.OBJECT && outputType.shareable) { return false; } } for (const interfaceName of typeDef.interfaces) { const iDef = stateBuilder.state.types.get(interfaceName); if (!iDef) { continue; } if (iDef.kind === TypeKind.INTERFACE && iDef.fields.has(fieldName)) { if (iDef.fields.has(fieldName)) { return false; } } } } return true; }); }, reportError(error) { reportedErrors.push(error); }, collectReportedErrors() { const errors = reportedErrors; reportedErrors = []; return errors; }, }; }