@apollo/federation-internals
Advanced tools
Comparing version 2.0.0-alpha.4 to 2.0.0-alpha.5
@@ -5,4 +5,10 @@ # CHANGELOG for `@apollo/federation-internals` | ||
> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. | ||
- _Nothing yet! Stay tuned._ | ||
## v2.0.0-alpha.5 | ||
- Remove `graphql@15` from peer dependencies [PR #1472](https://github.com/apollographql/federation/pull/1472). | ||
## v2.0.0-alpha.3 | ||
@@ -9,0 +15,0 @@ |
@@ -86,2 +86,3 @@ import { ASTNode, GraphQLError, Source } from "graphql"; | ||
EXTERNAL_MISSING_ON_BASE: ErrorCodeDefinition; | ||
INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH: ErrorCodeDefinition; | ||
SATISFIABILITY_ERROR: ErrorCodeDefinition; | ||
@@ -88,0 +89,0 @@ }; |
@@ -82,2 +82,3 @@ "use strict"; | ||
const EXTERNAL_MISSING_ON_BASE = makeCodeDefinition('EXTERNAL_MISSING_ON_BASE', 'A field is marked as `@external` in a subgraph but with no non-external declaration in any other subgraph.', { addedIn: FED1_CODE }); | ||
const INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH = makeCodeDefinition('INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH', 'For an interface field, some of its concrete implementations have @external or @requires and there is difference in those implementations return type (which is currently not supported; see https://github.com/apollographql/federation/issues/1257)'); | ||
const SATISFIABILITY_ERROR = makeCodeDefinition('SATISFIABILITY_ERROR', 'Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs.'); | ||
@@ -129,2 +130,3 @@ exports.ERROR_CATEGORIES = { | ||
EXTERNAL_MISSING_ON_BASE, | ||
INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH, | ||
SATISFIABILITY_ERROR, | ||
@@ -131,0 +133,0 @@ }; |
@@ -14,3 +14,2 @@ "use strict"; | ||
const error_1 = require("./error"); | ||
const _1 = require("."); | ||
exports.entityTypeName = '_Entity'; | ||
@@ -65,3 +64,3 @@ exports.serviceTypeName = '_Service'; | ||
if (field.hasArguments()) { | ||
throw _1.ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err({ | ||
throw error_1.ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err({ | ||
message: `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, | ||
@@ -73,3 +72,3 @@ nodes: field.sourceAST | ||
if (!isExternal && mustBeExternal) { | ||
const errorCode = _1.ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName); | ||
const errorCode = error_1.ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName); | ||
if (externalTester.isFakeExternal(field)) { | ||
@@ -134,3 +133,3 @@ throw errorCode.err({ | ||
} | ||
const codeDef = (_a = (0, error_1.errorCodeDef)(e)) !== null && _a !== void 0 ? _a : _1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name); | ||
const codeDef = (_a = (0, error_1.errorCodeDef)(e)) !== null && _a !== void 0 ? _a : error_1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name); | ||
return codeDef.err({ | ||
@@ -169,3 +168,3 @@ message: `${fieldSetErrorDescriptor(directive)}: ${e.message.trim()}`, | ||
if ((0, definitions_1.isInterfaceType)(parentType)) { | ||
const code = _1.ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name); | ||
const code = error_1.ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name); | ||
errorCollector.push(code.err({ | ||
@@ -206,2 +205,34 @@ message: isOnParentType | ||
} | ||
function validateInterfaceRuntimeImplementationFieldsTypes(itf, externalTester, errorCollector) { | ||
const runtimeTypes = itf.possibleRuntimeTypes(); | ||
for (const field of itf.fields()) { | ||
const withExternalOrRequires = []; | ||
const typeToImplems = new utils_1.MultiMap(); | ||
const nodes = []; | ||
for (const type of runtimeTypes) { | ||
const implemField = type.field(field.name); | ||
if (!implemField) | ||
continue; | ||
if (implemField.sourceAST) { | ||
nodes.push(implemField.sourceAST); | ||
} | ||
if (externalTester.isExternal(implemField) || implemField.hasAppliedDirective(exports.requiresDirectiveName)) { | ||
withExternalOrRequires.push(implemField); | ||
} | ||
const returnType = implemField.type; | ||
typeToImplems.add(returnType.toString(), implemField); | ||
} | ||
if (withExternalOrRequires.length > 0 && typeToImplems.size > 1) { | ||
const typeToImplemsArray = [...typeToImplems.entries()]; | ||
errorCollector.push(error_1.ERRORS.INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH.err({ | ||
message: `Some of the runtime implementations of interface field "${field.coordinate}" are marked @external or have a @require (${withExternalOrRequires.map(printFieldCoordinate)}) so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but ${formatFieldsToReturnType(typeToImplemsArray[0])} while ${(0, utils_1.joinStrings)(typeToImplemsArray.slice(1).map(formatFieldsToReturnType), ' and ')}.`, | ||
nodes | ||
})); | ||
} | ||
} | ||
} | ||
const printFieldCoordinate = (f) => `"${f.coordinate}"`; | ||
function formatFieldsToReturnType([type, implems]) { | ||
return `${(0, utils_1.joinStrings)(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`; | ||
} | ||
class FederationBuiltIns extends definitions_1.BuiltIns { | ||
@@ -284,3 +315,3 @@ addBuiltInTypes(schema) { | ||
if (existing) { | ||
errors.push(_1.ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({ | ||
errors.push(error_1.ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({ | ||
message: `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): ` | ||
@@ -329,2 +360,5 @@ + 'this is not supported by federation. ' | ||
} | ||
for (const itf of schema.types('InterfaceType')) { | ||
validateInterfaceRuntimeImplementationFieldsTypes(itf, externalTester, errors); | ||
} | ||
return errors; | ||
@@ -443,3 +477,3 @@ } | ||
} | ||
const codeDef = (_a = (0, error_1.errorCodeDef)(e)) !== null && _a !== void 0 ? _a : _1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name); | ||
const codeDef = (_a = (0, error_1.errorCodeDef)(e)) !== null && _a !== void 0 ? _a : error_1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name); | ||
throw codeDef.err({ | ||
@@ -458,3 +492,3 @@ message: `${fieldSetErrorDescriptor(directive)}: ${msg}`, | ||
if (typeof fields !== 'string') { | ||
throw _1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({ | ||
throw error_1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({ | ||
message: `Invalid value for argument "${directive.definition.argument('fields').name}": must be a string.`, | ||
@@ -468,3 +502,3 @@ nodes, | ||
if (argNode.value.kind !== 'StringValue') { | ||
throw _1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({ | ||
throw error_1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({ | ||
message: `Invalid value for argument "${directive.definition.argument('fields').name}": must be a string.`, | ||
@@ -471,0 +505,0 @@ nodes, |
@@ -43,2 +43,3 @@ export declare function assert(condition: any, message: string | (() => string)): asserts condition; | ||
export declare function validateStringContainsBoolean(str?: string): boolean | undefined; | ||
export declare function joinStrings(toJoin: string[], sep?: string, firstSep?: string, lastSep?: string): string; | ||
//# sourceMappingURL=utils.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.validateStringContainsBoolean = exports.copyWitNewLength = exports.MapWithCachedArrays = exports.setValues = exports.mapEntries = exports.mapKeys = exports.mapValues = exports.firstOf = exports.arrayEquals = exports.OrderedMap = exports.MultiMap = exports.assertUnreachable = exports.assert = void 0; | ||
exports.joinStrings = exports.validateStringContainsBoolean = exports.copyWitNewLength = exports.MapWithCachedArrays = exports.setValues = exports.mapEntries = exports.mapKeys = exports.mapValues = exports.firstOf = exports.arrayEquals = exports.OrderedMap = exports.MultiMap = exports.assertUnreachable = exports.assert = void 0; | ||
function assert(condition, message) { | ||
@@ -221,2 +221,17 @@ if (!condition) { | ||
exports.validateStringContainsBoolean = validateStringContainsBoolean; | ||
function joinStrings(toJoin, sep = ', ', firstSep, lastSep = ' and ') { | ||
if (toJoin.length == 0) { | ||
return ''; | ||
} | ||
const first = toJoin[0]; | ||
if (toJoin.length == 1) { | ||
return first; | ||
} | ||
const last = toJoin[toJoin.length - 1]; | ||
if (toJoin.length == 2) { | ||
return first + (firstSep ? firstSep : lastSep) + last; | ||
} | ||
return first + (firstSep ? firstSep : sep) + toJoin.slice(1, toJoin.length - 1) + lastSep + last; | ||
} | ||
exports.joinStrings = joinStrings; | ||
//# sourceMappingURL=utils.js.map |
@@ -6,3 +6,7 @@ const baseConfig = require('../jest.config.base'); | ||
module.exports = { | ||
...baseConfig | ||
...baseConfig, | ||
displayName: { | ||
name: '@apollo/federation-internals', | ||
color: 'magenta' | ||
} | ||
}; |
{ | ||
"name": "@apollo/federation-internals", | ||
"version": "2.0.0-alpha.4", | ||
"version": "2.0.0-alpha.5", | ||
"description": "Apollo Federation internal utilities", | ||
@@ -34,8 +34,5 @@ "main": "dist/index.js", | ||
"peerDependencies": { | ||
"graphql": "^15.7.0 || ^16.0.0" | ||
"graphql": "^16.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/js-levenshtein": "1.1.1" | ||
}, | ||
"gitHead": "c32794a48598bac2e2372c23a2ea74ae187cce85" | ||
"gitHead": "efb50aa3d3742d2fed8c841569651b2b7b729f2f" | ||
} |
@@ -453,1 +453,29 @@ import { DocumentNode } from 'graphql'; | ||
it('validates all implementations of interface field have same type if any has @external', () => { | ||
const subgraph = gql` | ||
type Query { | ||
is: [I!]! | ||
} | ||
interface I { | ||
f: Int | ||
} | ||
type T1 implements I { | ||
f: Int | ||
} | ||
type T2 implements I { | ||
f: Int! | ||
} | ||
type T3 implements I { | ||
id: ID! | ||
f: Int @external | ||
} | ||
`; | ||
expect(buildForErrors(subgraph)).toStrictEqual([ | ||
['INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH', '[S] Some of the runtime implementations of interface field "I.f" are marked @external or have a @require ("T3.f") so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but "T1.f" and "T3.f" have type "Int" while "T2.f" has type "Int!".'], | ||
]); | ||
}) |
@@ -267,2 +267,7 @@ import { ASTNode, GraphQLError, Source } from "graphql"; | ||
const INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH = makeCodeDefinition( | ||
'INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH', | ||
'For an interface field, some of its concrete implementations have @external or @requires and there is difference in those implementations return type (which is currently not supported; see https://github.com/apollographql/federation/issues/1257)' | ||
); | ||
const SATISFIABILITY_ERROR = makeCodeDefinition( | ||
@@ -319,2 +324,3 @@ 'SATISFIABILITY_ERROR', | ||
EXTERNAL_MISSING_ON_BASE, | ||
INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH, | ||
SATISFIABILITY_ERROR, | ||
@@ -321,0 +327,0 @@ }; |
@@ -29,3 +29,3 @@ import { | ||
} from "./definitions"; | ||
import { assert, OrderedMap } from "./utils"; | ||
import { assert, joinStrings, MultiMap, OrderedMap } from "./utils"; | ||
import { SDLValidationRule } from "graphql/validation/ValidationContext"; | ||
@@ -53,5 +53,5 @@ import { specifiedSDLRules } from "graphql/validation/specifiedRules"; | ||
ErrorCodeDefinition, | ||
ERROR_CATEGORIES, | ||
ERRORS, | ||
} from "./error"; | ||
import { ERROR_CATEGORIES } from "."; | ||
@@ -316,2 +316,48 @@ export const entityTypeName = '_Entity'; | ||
/** | ||
* Register errors when, for an interface field, some of the implementations of that field are @external | ||
* _and_ not all of those field implementation have the same type (which otherwise allowed because field | ||
* implementation types can be a subtype of the interface field they implement). | ||
* This is done because if that is the case, federation may later generate invalid query plans (see details | ||
* on https://github.com/apollographql/federation/issues/1257). | ||
* This "limitation" will be removed when we stop generating invalid query plans for it. | ||
*/ | ||
function validateInterfaceRuntimeImplementationFieldsTypes( | ||
itf: InterfaceType, | ||
externalTester: ExternalTester, | ||
errorCollector: GraphQLError[], | ||
): void { | ||
const runtimeTypes = itf.possibleRuntimeTypes(); | ||
for (const field of itf.fields()) { | ||
const withExternalOrRequires: FieldDefinition<ObjectType>[] = []; | ||
const typeToImplems: MultiMap<string, FieldDefinition<ObjectType>> = new MultiMap(); | ||
const nodes: ASTNode[] = []; | ||
for (const type of runtimeTypes) { | ||
const implemField = type.field(field.name); | ||
if (!implemField) continue; | ||
if (implemField.sourceAST) { | ||
nodes.push(implemField.sourceAST); | ||
} | ||
if (externalTester.isExternal(implemField) || implemField.hasAppliedDirective(requiresDirectiveName)) { | ||
withExternalOrRequires.push(implemField); | ||
} | ||
const returnType = implemField.type!; | ||
typeToImplems.add(returnType.toString(), implemField); | ||
} | ||
if (withExternalOrRequires.length > 0 && typeToImplems.size > 1) { | ||
const typeToImplemsArray = [...typeToImplems.entries()]; | ||
errorCollector.push(ERRORS.INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH.err({ | ||
message: `Some of the runtime implementations of interface field "${field.coordinate}" are marked @external or have a @require (${withExternalOrRequires.map(printFieldCoordinate)}) so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but ${formatFieldsToReturnType(typeToImplemsArray[0])} while ${joinStrings(typeToImplemsArray.slice(1).map(formatFieldsToReturnType), ' and ')}.`, | ||
nodes | ||
})); | ||
} | ||
} | ||
} | ||
const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`; | ||
function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) { | ||
return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`; | ||
} | ||
export class FederationBuiltIns extends BuiltIns { | ||
@@ -513,2 +559,6 @@ addBuiltInTypes(schema: Schema) { | ||
for (const itf of schema.types<InterfaceType>('InterfaceType')) { | ||
validateInterfaceRuntimeImplementationFieldsTypes(itf, externalTester, errors); | ||
} | ||
return errors; | ||
@@ -515,0 +565,0 @@ } |
@@ -272,1 +272,23 @@ /** | ||
} | ||
/** | ||
* Joins an array of string, much like `Array.prototype.join`, but with the ability to use a specific different | ||
* separator for the first and/or last occurence. | ||
* | ||
* The goal is to make reading flow slightly better. For instance, if you have a list of subgraphs `s = ["A", "B", "C"]`, | ||
* then `"subgraphs " + joinString(s)` will yield "subgraphs A, B and C". | ||
*/ | ||
export function joinStrings(toJoin: string[], sep: string = ', ', firstSep?: string, lastSep: string = ' and ') { | ||
if (toJoin.length == 0) { | ||
return ''; | ||
} | ||
const first = toJoin[0]; | ||
if (toJoin.length == 1) { | ||
return first; | ||
} | ||
const last = toJoin[toJoin.length - 1]; | ||
if (toJoin.length == 2) { | ||
return first + (firstSep ? firstSep : lastSep) + last; | ||
} | ||
return first + (firstSep ? firstSep : sep) + toJoin.slice(1, toJoin.length - 1) + lastSep + last; | ||
} |
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
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
1209622
0
19407