Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@apollo/composition

Package Overview
Dependencies
Maintainers
1
Versions
131
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@apollo/composition - npm Package Compare versions

Comparing version 2.0.0-alpha.6 to 2.0.0-preview.0

src/__tests__/composeFed1Subgraphs.test.ts

11

dist/compose.js

@@ -9,3 +9,12 @@ "use strict";

function compose(subgraphs) {
const mergeResult = (0, merging_1.mergeSubgraphs)(subgraphs);
const upgradeResult = (0, federation_internals_1.upgradeSubgraphsIfNecessary)(subgraphs);
if (upgradeResult.errors) {
return { errors: upgradeResult.errors };
}
const toMerge = upgradeResult.subgraphs;
const validationErrors = toMerge.validate();
if (validationErrors) {
return { errors: validationErrors };
}
const mergeResult = (0, merging_1.mergeSubgraphs)(toMerge);
if (mergeResult.errors) {

@@ -12,0 +21,0 @@ return { errors: mergeResult.errors };

4

dist/hints.js

@@ -37,4 +37,4 @@ "use strict";

exports.hintInconsistentDescription = new HintID('InconsistentDescription', 'Indicates that an element has a description in more than one subgraph, and the descriptions are not equal', 'the element with inconsistent description');
exports.hintInconsistentArgumentPresence = new HintID('InconsistentArgumentPresence', 'Indicates that an argument of an execution directive definition is not present in all subgraphs '
+ 'and will not be part of the supergraph', 'the argument with mismatched types');
exports.hintInconsistentArgumentPresence = new HintID('InconsistentArgumentPresence', 'Indicates that an (optional) argument (of a field or directive definition) is not present in all subgraphs '
+ ' and will not be part of the supergraph', 'the argument with mismatched types');
class CompositionHint {

@@ -41,0 +41,0 @@ constructor(id, message, elementCoordinate, nodes) {

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {

@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k;

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {

@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k;

@@ -9,4 +9,3 @@ "use strict";

const tagSpec = federation_internals_1.TAG_VERSIONS.latest();
const MAX_HUMAN_READABLE_LIST_LENGTH = 100;
const MERGED_TYPE_SYSTEM_DIRECTIVES = ['inaccessible', 'deprecated', 'specifiedBy', 'tag'];
const MERGED_TYPE_SYSTEM_DIRECTIVES = [];
const defaultCompositionOptions = {

@@ -24,29 +23,6 @@ allowedFieldTypeMergingSubtypingRules: federation_internals_1.DEFAULT_SUBTYPING_RULES

function mergeSubgraphs(subgraphs, options = {}) {
(0, federation_internals_1.assert)(subgraphs.values().every((s) => s.isFed2Subgraph()), 'Merging should only be applied to federation 2 subgraphs');
return new Merger(subgraphs, { ...defaultCompositionOptions, ...options }).merge();
}
exports.mergeSubgraphs = mergeSubgraphs;
function printHumanReadableList(names, prefixSingle, prefixPlural) {
(0, federation_internals_1.assert)(names.length > 0, 'Should not have been called with no names');
if (names.length == 1) {
return prefixSingle ? prefixSingle + ' ' + names[0] : names[0];
}
let toDisplay = names;
let totalLength = toDisplay.reduce((count, name) => count + name.length, 0);
while (totalLength > MAX_HUMAN_READABLE_LIST_LENGTH && toDisplay.length > 1) {
toDisplay = toDisplay.slice(0, toDisplay.length - 1);
totalLength = toDisplay.reduce((count, name) => count + name.length, 0);
}
const prefix = prefixPlural
? prefixPlural + ' '
: (prefixSingle ? prefixSingle + ' ' : '');
if (toDisplay.length === names.length) {
return prefix + (0, federation_internals_1.joinStrings)(toDisplay);
}
else {
return prefix + toDisplay.join(', ') + ', ...';
}
}
function printSubgraphNames(names) {
return printHumanReadableList(names.map(n => `"${n}"`), 'subgraph', 'subgraphs');
}
function copyTypeReference(source, dest) {

@@ -64,4 +40,11 @@ switch (source.kind) {

}
const NON_MERGED_CORE_FEATURES = [federation_internals_1.federationIdentity, federation_internals_1.linkIdentity, federation_internals_1.coreIdentity];
function isMergedType(type) {
return !(0, federation_internals_1.isFederationType)(type) && !type.isIntrospectionType();
var _a;
if (type.isIntrospectionType() || federation_internals_1.FEDERATION_OPERATION_TYPES.map((s) => s.name).includes(type.name)) {
return false;
}
const coreFeatures = type.schema().coreFeatures;
const typeFeature = (_a = coreFeatures === null || coreFeatures === void 0 ? void 0 : coreFeatures.sourceFeature(type)) === null || _a === void 0 ? void 0 : _a.url.identity;
return !(typeFeature && NON_MERGED_CORE_FEATURES.includes(typeFeature));
}

@@ -71,2 +54,5 @@ function isMergedField(field) {

}
function isGraphQLBuiltInDirective(def) {
return !!def.schema().builtInDirective(def.name);
}
function isMergedDirective(definition) {

@@ -77,2 +63,5 @@ if (MERGED_TYPE_SYSTEM_DIRECTIVES.includes(definition.name)) {

if (definition instanceof federation_internals_1.Directive) {
return isGraphQLBuiltInDirective(definition.definition);
}
else if (isGraphQLBuiltInDirective(definition)) {
return false;

@@ -114,4 +103,3 @@ }

function hasTagUsage(subgraph) {
const directive = subgraph.directive(federation_internals_1.tagDirectiveName);
return !!directive && directive.applications().length > 0;
return subgraph.metadata().tagDirective().applications().length > 0;
}

@@ -134,3 +122,2 @@ function locationString(locations) {

this.subgraphNamesToJoinSpecName = this.prepareSupergraph();
this.externalTesters = this.subgraphsSchema.map(schema => new federation_internals_1.ExternalTester(schema));
}

@@ -140,3 +127,3 @@ prepareSupergraph() {

coreSpec.applyFeatureToSchema(this.merged, joinSpec, undefined, 'EXECUTION');
if (this.subgraphsSchema.some(hasTagUsage)) {
if (this.subgraphs.values().some(hasTagUsage)) {
coreSpec.applyFeatureToSchema(this.merged, tagSpec);

@@ -149,2 +136,5 @@ }

}
metadata(idx) {
return this.subgraphs.values()[idx].metadata();
}
merge() {

@@ -290,3 +280,3 @@ this.addTypesShallow();

const subgraphsLikeSupergraph = distributionMap.get(supergraphMismatch);
distribution.push(supergraphElementPrinter(supergraphMismatch, subgraphsLikeSupergraph ? printSubgraphNames(subgraphsLikeSupergraph) : undefined));
distribution.push(supergraphElementPrinter(supergraphMismatch, subgraphsLikeSupergraph ? (0, federation_internals_1.printSubgraphNames)(subgraphsLikeSupergraph) : undefined));
for (const [v, names] of distributionMap.entries()) {

@@ -296,3 +286,3 @@ if (v === supergraphMismatch) {

}
distribution.push(otherElementsPrinter(v === '' ? undefined : v, printSubgraphNames(names)));
distribution.push(otherElementsPrinter(v === '' ? undefined : v, (0, federation_internals_1.printSubgraphNames)(names)));
}

@@ -411,4 +401,4 @@ reporter(distribution, astNodes);

}
const sourceSchema = this.subgraphsSchema[idx];
const keys = source.appliedDirectivesOf(federation_internals_1.federationBuiltIns.keyDirective(sourceSchema));
const sourceMetadata = this.subgraphs.values()[idx].metadata();
const keys = source.appliedDirectivesOf(sourceMetadata.keyDirective());
const name = this.joinSpecName(idx);

@@ -420,4 +410,5 @@ if (!keys.length) {

for (const key of keys) {
const extension = key.ofExtension() || source.hasAppliedDirective(federation_internals_1.federationBuiltIns.extendsDirective(sourceSchema)) ? true : undefined;
dest.applyDirective(joinTypeDirective, { graph: name, key: key.arguments().fields, extension });
const extension = key.ofExtension() || source.hasAppliedDirective(sourceMetadata.extendsDirective()) ? true : undefined;
const { resolvable } = key.arguments();
dest.applyDirective(joinTypeDirective, { graph: name, key: key.arguments().fields, extension, resolvable });
}

@@ -441,2 +432,3 @@ }

this.mergeField(subgraphFields, destField);
this.validateFieldSharing(subgraphFields, destField);
}

@@ -503,4 +495,7 @@ }

isExternal(sourceIdx, field) {
return this.externalTesters[sourceIdx].isExternal(field);
return this.metadata(sourceIdx).isFieldExternal(field);
}
isFullyExternal(sourceIdx, field) {
return this.metadata(sourceIdx).isFieldFullyExternal(field);
}
withoutExternal(sources) {

@@ -512,2 +507,5 @@ return sources.map((s, i) => s !== undefined && this.isExternal(i, s) ? undefined : s);

}
isShareable(sourceIdx, field) {
return this.metadata(sourceIdx).isFieldShareable(field);
}
mergeField(sources, dest) {

@@ -518,3 +516,3 @@ if (sources.every((s, i) => s === undefined || this.isExternal(i, s))) {

this.errors.push(federation_internals_1.ERRORS.EXTERNAL_MISSING_ON_BASE.err({
message: `Field "${dest.coordinate}" is marked @external on all the subgraphs in which it is listed (${printSubgraphNames(definingSubgraphs)}).`,
message: `Field "${dest.coordinate}" is marked @external on all the subgraphs in which it is listed (${(0, federation_internals_1.printSubgraphNames)(definingSubgraphs)}).`,
nodes

@@ -538,2 +536,29 @@ }));

}
validateFieldSharing(sources, dest) {
const shareableSources = [];
const nonShareableSources = [];
const allResolving = [];
for (const [i, source] of sources.entries()) {
if (!source || this.isFullyExternal(i, source)) {
continue;
}
allResolving.push(source);
if (this.isShareable(i, source)) {
shareableSources.push(i);
}
else {
nonShareableSources.push(i);
}
}
if (nonShareableSources.length > 0 && (shareableSources.length > 0 || nonShareableSources.length > 1)) {
const resolvingSubgraphs = nonShareableSources.concat(shareableSources).map((s) => this.names[s]);
const nonShareables = shareableSources.length > 0
? (0, federation_internals_1.printSubgraphNames)(nonShareableSources.map((s) => this.names[s]))
: 'all of them';
this.errors.push(federation_internals_1.ERRORS.INVALID_FIELD_SHARING.err({
message: `Non-shareable field "${dest.coordinate}" is resolved from multiple subgraphs: it is resolved from ${(0, federation_internals_1.printSubgraphNames)(resolvingSubgraphs)} and defined as non-shareable in ${nonShareables}`,
nodes: (0, federation_internals_1.sourceASTs)(...allResolving),
}));
}
}
validateExternalFields(sources, dest, allTypesEqual) {

@@ -588,5 +613,6 @@ let hasInvalidTypes = false;

if (source) {
const sourceMeta = this.subgraphs.values()[idx].metadata();
if (this.isExternal(idx, source)
|| source.hasAppliedDirective(federation_internals_1.providesDirectiveName)
|| source.hasAppliedDirective(federation_internals_1.requiresDirectiveName)) {
|| source.hasAppliedDirective(sourceMeta.providesDirective())
|| source.hasAppliedDirective(sourceMeta.requiresDirective())) {
return true;

@@ -614,7 +640,8 @@ }

const external = this.isExternal(idx, source);
const sourceMeta = this.subgraphs.values()[idx].metadata();
const name = this.joinSpecName(idx);
dest.applyDirective(joinFieldDirective, {
graph: name,
requires: this.getFieldSet(source, federation_internals_1.federationBuiltIns.requiresDirective(this.subgraphsSchema[idx])),
provides: this.getFieldSet(source, federation_internals_1.federationBuiltIns.providesDirective(this.subgraphsSchema[idx])),
requires: this.getFieldSet(source, sourceMeta.requiresDirective()),
provides: this.getFieldSet(source, sourceMeta.providesDirective()),
type: allTypesEqual ? undefined : (_a = source.type) === null || _a === void 0 ? void 0 : _a.toString(),

@@ -676,2 +703,3 @@ external: external ? true : undefined,

addArgumentsShallow(sources, dest) {
const argNames = new Set();
for (const source of sources) {

@@ -681,19 +709,24 @@ if (!source) {

}
for (const argument of source.arguments()) {
if (!dest.argument(argument.name)) {
dest.addArgument(argument.name);
source.arguments().forEach((arg) => argNames.add(arg.name));
}
for (const argName of argNames) {
const arg = dest.addArgument(argName);
if (sources.some((s) => s && !s.argument(argName))) {
const nonOptionalSources = sources.map((s, i) => { var _a; return s && ((_a = s.argument(argName)) === null || _a === void 0 ? void 0 : _a.isRequired()) ? this.names[i] : undefined; }).filter((s) => !!s);
if (nonOptionalSources.length > 0) {
const nonOptionalSubgraphs = (0, federation_internals_1.printSubgraphNames)(nonOptionalSources);
const missingSources = (0, federation_internals_1.printSubgraphNames)(sources.map((s, i) => s && !s.argument(argName) ? this.names[i] : undefined).filter((s) => !!s));
this.errors.push(federation_internals_1.ERRORS.REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH.err({
message: `Argument "${arg.coordinate}" is required in some subgraphs but does not appear in all subgraphs: it is required in ${nonOptionalSubgraphs} but does not appear in ${missingSources}`,
nodes: (0, federation_internals_1.sourceASTs)(...sources.map((s) => s === null || s === void 0 ? void 0 : s.argument(argName))),
}));
}
else {
this.reportMismatchHint(hints_1.hintInconsistentArgumentPresence, `Argument "${arg.coordinate}" will not be added to "${dest}" in the supergraph as it does not appear in all subgraphs: `, arg, sources.map((s) => s ? s.argument(argName) : undefined), _ => 'yes', (_, subgraphs) => `it is defined in ${subgraphs}`, (_, subgraphs) => ` but not in ${subgraphs}`, undefined, true);
}
arg.remove();
}
}
}
mergeArgument(sources, dest, useIntersection = false) {
if (useIntersection) {
for (const source of sources) {
if (!source) {
this.reportMismatchHint(hints_1.hintInconsistentArgumentPresence, `Argument "${dest.coordinate}" will not be added to "${dest.parent}" in the supergraph as it does not appear in all subgraphs: `, dest, sources, _ => 'yes', (_, subgraphs) => `it is defined in ${subgraphs}`, (_, subgraphs) => ` but not in ${subgraphs}`, undefined, true);
dest.remove();
return;
}
}
}
mergeArgument(sources, dest) {
this.mergeDescription(sources, dest);

@@ -814,3 +847,3 @@ this.mergeAppliedDirectives(sources, dest);

}
else {
else if (sources.some((s) => s && isMergedDirective(s))) {
this.mergeExecutionDirectiveDefinition(sources, dest);

@@ -910,3 +943,3 @@ }

const subgraphArgs = sources.map(f => f === null || f === void 0 ? void 0 : f.argument(destArg.name));
this.mergeArgument(subgraphArgs, destArg, true);
this.mergeArgument(subgraphArgs, destArg);
}

@@ -943,2 +976,3 @@ }

mergeAppliedDirective(name, sources, dest) {
var _a, _b;
let perSource = [];

@@ -956,4 +990,18 @@ for (const source of sources) {

const directive = this.pickNextDirective(perSource);
dest.applyDirective(directive.name, directive.arguments(false));
perSource = this.removeDirective(directive, perSource);
if (!((_a = directive.definition) === null || _a === void 0 ? void 0 : _a.repeatable) && dest.hasAppliedDirective(directive.name)) {
this.reportMismatchError(federation_internals_1.ERRORS.NON_REPEATABLE_DIRECTIVE_ARGUMENTS_MISMATCH, `Non-repeatable directive @${directive.name} is applied to "${(_b = dest['coordinate']) !== null && _b !== void 0 ? _b : dest}" in multiple subgraphs but with incompatible arguments: it uses `, dest, sources, (elt) => {
var _a;
const args = (_a = elt.appliedDirectivesOf(directive.name).pop()) === null || _a === void 0 ? void 0 : _a.arguments();
return args === undefined
? undefined
: Object.values(args).length === 0 ? 'no arguments' : (`arguments ${(0, federation_internals_1.valueToString)(args)}`);
});
perSource = perSource
.map((ds) => ds.filter((d) => d.name !== directive.name))
.filter((ds) => ds.length > 0);
}
else {
dest.applyDirective(directive.name, directive.arguments(false));
perSource = this.removeDirective(directive, perSource);
}
}

@@ -1014,4 +1062,4 @@ }

this.errors.push(federation_internals_1.ERRORS.INTERFACE_FIELD_NO_IMPLEM.err({
message: `Interface field "${itfField.coordinate}" is declared in ${printSubgraphNames(subgraphsWithTheField)} but type "${type}", `
+ `which implements "${itf}" only in ${printSubgraphNames(subgraphsWithTypeImplementingItf)} does not have field "${itfField.name}".`,
message: `Interface field "${itfField.coordinate}" is declared in ${(0, federation_internals_1.printSubgraphNames)(subgraphsWithTheField)} but type "${type}", `
+ `which implements "${itf}" only in ${(0, federation_internals_1.printSubgraphNames)(subgraphsWithTypeImplementingItf)} does not have field "${itfField.name}".`,
nodes: (0, federation_internals_1.sourceASTs)(...subgraphsWithTheField.map(s => { var _a; return (_a = this.subgraphByName(s).typeOfKind(itf.name, 'InterfaceType')) === null || _a === void 0 ? void 0 : _a.field(itfField.name); }), ...subgraphsWithTypeImplementingItf.map(s => this.subgraphByName(s).type(type.name)))

@@ -1018,0 +1066,0 @@ }));

{
"name": "@apollo/composition",
"version": "2.0.0-alpha.6",
"version": "2.0.0-preview.0",
"description": "Apollo Federation composition utilities",

@@ -30,4 +30,4 @@ "main": "dist/index.js",

"dependencies": {
"@apollo/federation-internals": "^2.0.0-alpha.6",
"@apollo/query-graphs": "^2.0.0-alpha.6"
"@apollo/federation-internals": "^2.0.0-preview.0",
"@apollo/query-graphs": "^2.0.0-preview.0"
},

@@ -37,3 +37,3 @@ "peerDependencies": {

},
"gitHead": "ee84b3fd9df161c3a94ae3d70e82a14dba5794ba"
"gitHead": "e76fd3e16bf140ba9b30fe26d48657bccca180dc"
}

@@ -1,2 +0,2 @@

import { buildSchema, extractSubgraphsFromSupergraph, ObjectType, printSchema, Schema, Subgraphs } from '@apollo/federation-internals';
import { asFed2SubgraphDocument, buildSchema, extractSubgraphsFromSupergraph, ObjectType, printSchema, Schema, ServiceDefinition, Subgraphs } from '@apollo/federation-internals';
import { CompositionResult, composeServices, CompositionSuccess } from '../compose';

@@ -24,2 +24,11 @@ import gql from 'graphql-tag';

// Note that tests for composition involving fed1 subgraph are in `composeFed1Subgraphs.test.ts` so all the test of this
// file are on fed2 subgraphs, but to avoid needing to add the proper `@link(...)` everytime, we inject it here automatically.
export function composeAsFed2Subgraphs(services: ServiceDefinition[]): CompositionResult {
return composeServices(services.map((s) => ({
...s,
typeDefs: asFed2SubgraphDocument(s.typeDefs)
})));
}
describe('composition', () => {

@@ -53,3 +62,3 @@ it('generates a valid supergraph', () => {

const result = composeServices([subgraph1, subgraph2]);
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
assertCompositionSuccess(result);

@@ -73,3 +82,3 @@

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

@@ -162,3 +171,3 @@ enum core__Purpose {

const result = composeServices([subgraph1, subgraph2]);
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
assertCompositionSuccess(result);

@@ -221,3 +230,3 @@

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -243,3 +252,10 @@ assertCompositionSuccess(result);

expect(printSchema(subgraphs.get('subgraphA')!.schema)).toMatchString(`
expect(subgraphs.get('subgraphA')!.toString()).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@shareable", "@tag", "@extends"])
{
query: Query
}
type Product {

@@ -255,3 +271,10 @@ sku: String!

expect(printSchema(subgraphs.get('subgraphB')!.schema)).toMatchString(`
expect(subgraphs.get('subgraphB')!.toString()).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@shareable", "@tag", "@extends"])
{
query: Query
}
type User {

@@ -283,3 +306,3 @@ name: String

sku: String!
name: String!
name: String! @shareable
}

@@ -290,3 +313,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -307,3 +330,10 @@

// Of course, the federation directives should be rebuilt in the extracted subgraphs.
expect(printSchema(subgraphs.get('subgraphA')!.schema)).toMatchString(`
expect(subgraphs.get('subgraphA')!.toString()).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@shareable", "@tag", "@extends"])
{
query: Query
}
type Product

@@ -321,3 +351,10 @@ @key(fields: "sku")

expect(printSchema(subgraphs.get('subgraphB')!.schema)).toMatchString(`
expect(subgraphs.get('subgraphB')!.toString()).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@shareable", "@tag", "@extends"])
{
query: Query
}
type Product

@@ -332,228 +369,2 @@ @key(fields: "sku")

describe('basic type extensions', () => {
it('works when extension subgraph is second', () => {
const subgraphA = {
typeDefs: gql`
type Query {
products: [Product!]
}
type Product @key(fields: "sku") {
sku: String!
name: String!
}
`,
name: 'subgraphA',
};
// Note that putting @external on the key is now frown upon, but putting it there for a fed1-compatible example.
const subgraphB = {
typeDefs: gql`
extend type Product @key(fields: "sku") {
sku: String! @external
price: Int!
}
`,
name: 'subgraphB',
};
const result = composeServices([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [_, api, subgraphs] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Product {
sku: String!
name: String!
price: Int!
}
type Query {
products: [Product!]
}
`);
expect(printSchema(subgraphs.get('subgraphA')!.schema)).toMatchString(`
type Product
@key(fields: "sku")
{
sku: String!
name: String!
}
type Query {
products: [Product!]
}
`);
// Note that while this is a weird-looking schema, this is what is extract from
// the supergraph as we don't preserve enough information to say that the whole type
// was defined as an extension, only that the key part on an extension (the reason
// being that it's the only thing we truly need as the handling of @external on
// key fields of extension differs from that of non-extension, but nothing else
// does).
expect(printSchema(subgraphs.get('subgraphB')!.schema)).toMatchString(`
type Product {
sku: String!
price: Int!
}
extend type Product
@key(fields: "sku")
`);
});
it('works when extension subgraph is first', () => {
// Note that putting @external on the key is now frown upon, but putting it there for a fed1-compatible example.
const subgraphA = {
typeDefs: gql`
extend type Product @key(fields: "sku") {
sku: String! @external
price: Int!
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type Query {
products: [Product!]
}
type Product @key(fields: "sku") {
sku: String!
name: String!
}
`,
name: 'subgraphB',
};
const result = composeServices([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [_, api, subgraphs] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Product {
sku: String!
price: Int!
name: String!
}
type Query {
products: [Product!]
}
`);
// Same remark than in prevoius test
expect(printSchema(subgraphs.get('subgraphA')!.schema)).toMatchString(`
type Product {
sku: String!
price: Int!
}
extend type Product
@key(fields: "sku")
`);
expect(printSchema(subgraphs.get('subgraphB')!.schema)).toMatchString(`
type Product
@key(fields: "sku")
{
sku: String!
name: String!
}
type Query {
products: [Product!]
}
`);
});
it('works with multiple extensions on the same type', () => {
const subgraphA = {
typeDefs: gql`
extend type Product @key(fields: "sku") {
sku: String!
price: Int!
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type Query {
products: [Product!]
}
type Product {
sku: String!
name: String!
}
`,
name: 'subgraphB',
};
const subgraphC = {
typeDefs: gql`
extend type Product @key(fields: "sku") {
sku: String!
color: String!
}
`,
name: 'subgraphC',
};
const result = composeServices([subgraphA, subgraphB, subgraphC]);
assertCompositionSuccess(result);
const [_, api, subgraphs] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Product {
sku: String!
price: Int!
name: String!
color: String!
}
type Query {
products: [Product!]
}
`);
expect(printSchema(subgraphs.get('subgraphA')!.schema)).toMatchString(`
type Product {
sku: String!
price: Int!
}
extend type Product
@key(fields: "sku")
`);
expect(printSchema(subgraphs.get('subgraphB')!.schema)).toMatchString(`
type Product {
sku: String!
name: String!
}
type Query {
products: [Product!]
}
`);
expect(printSchema(subgraphs.get('subgraphC')!.schema)).toMatchString(`
type Product {
sku: String!
color: String!
}
extend type Product
@key(fields: "sku")
`);
});
});
describe('merging of type references', () => {

@@ -571,3 +382,3 @@ describe('for field types', () => {

id: ID!
f: String
f: String @shareable
}

@@ -582,3 +393,3 @@ `,

id: ID!
f: Int
f: Int @shareable
}

@@ -588,3 +399,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -616,3 +427,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f: Int
f: Int @shareable
}

@@ -622,3 +433,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -640,3 +451,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f: String
f: String @shareable
}

@@ -651,3 +462,3 @@ `,

id: ID!
f: [String]
f: [String] @shareable
}

@@ -657,3 +468,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -675,3 +486,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f: String!
f: String! @shareable
}

@@ -686,3 +497,3 @@ `,

id: ID!
f: String
f: String @shareable
}

@@ -692,3 +503,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -722,3 +533,3 @@

type A implements I {
type A implements I @shareable {
a: Int

@@ -735,3 +546,3 @@ b: Int

id: ID!
f: I
f: I @shareable
}

@@ -744,3 +555,3 @@ `,

typeDefs: gql`
type A {
type A @shareable {
a: Int

@@ -752,3 +563,3 @@ b: Int

id: ID!
f: A
f: A @shareable
}

@@ -758,3 +569,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -809,3 +620,3 @@

type A {
type A @shareable {
a: Int

@@ -820,3 +631,3 @@ }

id: ID!
f: U
f: U @shareable
}

@@ -829,3 +640,3 @@ `,

typeDefs: gql`
type A {
type A @shareable {
a: Int

@@ -836,3 +647,3 @@ }

id: ID!
f: A
f: A @shareable
}

@@ -842,3 +653,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -893,3 +704,3 @@

type A implements I {
type A implements I @shareable {
a: Int

@@ -906,3 +717,3 @@ b: Int

id: ID!
f: I
f: I @shareable
}

@@ -915,3 +726,3 @@ `,

typeDefs: gql`
type A {
type A @shareable {
a: Int

@@ -923,3 +734,3 @@ b: Int

id: ID!
f: A!
f: A! @shareable
}

@@ -929,3 +740,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -984,3 +795,3 @@

type A implements I {
type A implements I @shareable {
a: Int

@@ -997,3 +808,3 @@ b: Int

id: ID!
f: [I]
f: [I] @shareable
}

@@ -1006,3 +817,3 @@ `,

typeDefs: gql`
type A {
type A @shareable {
a: Int

@@ -1014,3 +825,3 @@ b: Int

id: ID!
f: [A!]
f: [A!] @shareable
}

@@ -1020,3 +831,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -1075,3 +886,3 @@

type A implements I {
type A implements I @shareable {
a: Int

@@ -1088,3 +899,3 @@ b: Int

id: ID!
f: I!
f: I! @shareable
}

@@ -1097,3 +908,3 @@ `,

typeDefs: gql`
type A {
type A @shareable {
a: Int

@@ -1105,3 +916,3 @@ b: Int

id: ID!
f: A!
f: A! @shareable
}

@@ -1111,3 +922,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -1175,3 +986,3 @@

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1206,3 +1017,3 @@ expect(errors(result)).toStrictEqual([

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1227,3 +1038,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f(x: Int): Int
f(x: Int): Int @shareable
}

@@ -1238,3 +1049,3 @@ `,

id: ID!
f(x: String): Int
f(x: String): Int @shareable
}

@@ -1244,3 +1055,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1272,3 +1083,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f(x: Int): String
f(x: Int): String @shareable
}

@@ -1278,3 +1089,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1315,3 +1126,3 @@ expect(errors(result)).toStrictEqual([

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1333,3 +1144,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f(x: Int = 0): String
f(x: Int = 0): String @shareable
}

@@ -1344,3 +1155,3 @@ `,

id: ID!
f(x: Int = 1): String
f(x: Int = 1): String @shareable
}

@@ -1350,3 +1161,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1387,3 +1198,3 @@ expect(errors(result)).toStrictEqual([

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1405,3 +1216,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f(x: String): String
f(x: String): String @shareable
}

@@ -1416,3 +1227,3 @@ `,

id: ID!
f(x: [String]): String
f(x: [String]): String @shareable
}

@@ -1422,3 +1233,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -1440,3 +1251,3 @@ expect(errors(result)).toStrictEqual([

id: ID!
f(x: String): String
f(x: String): String @shareable
}

@@ -1451,3 +1262,3 @@ `,

id: ID!
f(x: String!): String
f(x: String!): String @shareable
}

@@ -1457,3 +1268,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -1487,3 +1298,3 @@

id: ID!
f(x: [Int]): Int
f(x: [Int]): Int @shareable
}

@@ -1498,3 +1309,3 @@ `,

id: ID!
f(x: [Int!]): Int
f(x: [Int!]): Int @shareable
}

@@ -1504,3 +1315,3 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -1544,3 +1355,3 @@

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -1574,7 +1385,7 @@ expect(result.errors).toBeDefined();

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['TAG_DIRECTIVE_DEFINITION_INVALID', '[subgraphA] Found invalid @tag directive definition. Please ensure the directive definition in your schema\'s definitions matches the following:\n\tdirective @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'],
['DIRECTIVE_DEFINITION_INVALID', '[subgraphA] Found invalid @tag directive definition. Please ensure the directive definition in your schema\'s definitions matches the following:\n\tdirective @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'],
]);

@@ -1602,7 +1413,7 @@ });

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_SUBGRAPH_NAME', 'Invalid name _ for a subgraph: this name is reserved'],
['INVALID_SUBGRAPH_NAME', '[_] Invalid name _ for a subgraph: this name is reserved'],
]);

@@ -1630,3 +1441,3 @@ });

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -1662,3 +1473,3 @@ expect(result.errors).toBeDefined();

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -1671,3 +1482,3 @@ expect(result.errors).toBeDefined();

it('errors if a type extension has no definition counterpart', () => {
it('errors if an @external field is not defined in any other subgraph', () => {
const subgraphA = {

@@ -1684,4 +1495,9 @@ typeDefs: gql`

typeDefs: gql`
extend type A @key(fields: "k") {
interface I {
f: Int
}
type A implements I @key(fields: "k") {
k: ID!
f: Int @external
}

@@ -1692,15 +1508,15 @@ `,

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['EXTENSION_WITH_NO_BASE', '[subgraphB] Type "A" is an extension type, but there is no type definition for "A" in any subgraph.'],
['EXTERNAL_MISSING_ON_BASE', 'Field "A.f" is marked @external on all the subgraphs in which it is listed (subgraph "subgraphB").'],
]);
});
it('errors if an @external field is not defined in any other subgraph', () => {
it('errors if a mandatory argument is not in all subgraphs', () => {
const subgraphA = {
typeDefs: gql`
type Query {
q: String
q(a: Int!): String @shareable
}

@@ -1713,10 +1529,5 @@ `,

typeDefs: gql`
interface I {
f: Int
type Query {
q: String @shareable
}
type A implements I @key(fields: "k") {
k: ID!
f: Int @external
}
`,

@@ -1726,7 +1537,8 @@ name: 'subgraphB',

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['EXTERNAL_MISSING_ON_BASE', 'Field "A.f" is marked @external on all the subgraphs in which it is listed (subgraph "subgraphB").'],
['REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH',
'Argument "Query.q(a:)" is required in some subgraphs but does not appear in all subgraphs: it is required in subgraph "subgraphA" but does not appear in subgraph "subgraphB"']
]);

@@ -1769,3 +1581,3 @@ });

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -1815,3 +1627,3 @@ expect(result.errors).toBeDefined();

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);

@@ -1825,2 +1637,127 @@ expect(result.errors).toBeDefined();

describe('merging of directives', () => {
it('propagates graphQL built-in directives', () => {
const subgraphA = {
name: 'subgraphA',
typeDefs: gql`
type Query {
a: String @shareable @deprecated(reason: "bad")
}
`,
};
const subgraphB = {
name: 'subgraphB',
typeDefs: gql`
type Query {
a: String @shareable
}
`,
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [_, api] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Query {
a: String @deprecated(reason: "bad")
}
`);
});
it('merges graphQL built-in directives', () => {
const subgraphA = {
name: 'subgraphA',
typeDefs: gql`
type Query {
a: String @shareable @deprecated(reason: "bad")
}
`,
};
const subgraphB = {
name: 'subgraphB',
typeDefs: gql`
type Query {
a: String @shareable @deprecated(reason: "bad")
}
`,
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [_, api] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Query {
a: String @deprecated(reason: "bad")
}
`);
});
it('errors on incompatible non-repeatable (built-in) directives', () => {
const subgraphA = {
name: 'subgraphA',
typeDefs: gql`
type Query {
a: String @shareable @deprecated(reason: "bad")
}
`,
};
const subgraphB = {
name: 'subgraphB',
typeDefs: gql`
type Query {
a: String @shareable @deprecated
}
`,
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['NON_REPEATABLE_DIRECTIVE_ARGUMENTS_MISMATCH',
'Non-repeatable directive @deprecated is applied to "Query.a" in multiple subgraphs but with incompatible arguments: it uses arguments {reason: "bad"} in subgraph "subgraphA" but no arguments in subgraph "subgraphB"'],
]);
});
it('propagates graphQL built-in directives even if redefined in the subgarph', () => {
const subgraphA = {
name: 'subgraphA',
typeDefs: gql`
type Query {
a: String @deprecated
}
# Do note that the code validates that this definition below
# is "compatible" with the "real one", which it is.
directive @deprecated on FIELD_DEFINITION
`,
};
const subgraphB = {
name: 'subgraphB',
typeDefs: gql`
type Query {
b: String
}
`,
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [_, api] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Query {
a: String @deprecated
b: String
}
`);
});
});
it('is not broken by similar field argument signatures (#1100)', () => {

@@ -1835,3 +1772,3 @@ // This test is about validating the case from https://github.com/apollographql/federation/issues/1100 is fixed.

type T {
type T @shareable {
a(x: String): Int

@@ -1846,3 +1783,3 @@ b(x: Int): Int

typeDefs: gql`
type T {
type T @shareable {
a(x: String): Int

@@ -1855,3 +1792,3 @@ b(x: Int): Int

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);

@@ -1874,3 +1811,3 @@

// just check the associated error code is correct (since we check most composition error
// codes in this fiel)
// codes in this file)
it('use the proper error code for composition validation errors', () => {

@@ -1883,3 +1820,3 @@ const subgraphA = {

type A {
type A @shareable {
x: Int

@@ -1893,3 +1830,3 @@ }

typeDefs: gql`
type A {
type A @shareable {
x: Int

@@ -1902,18 +1839,319 @@ y: Int

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['SATISFIABILITY_ERROR', `The following supergraph API query:
{
a {
y
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "subgraphA":
- cannot find field "A.y".
- cannot move to subgraph "subgraphB", which has field "A.y", because type "A" has no @key defined in subgraph "subgraphB".`],
]);
expect(errors(result).map(([code]) => code)).toStrictEqual(['SATISFIABILITY_ERROR']);
expect(errors(result).map(([_, msg]) => msg)).toMatchStringArray([
`
The following supergraph API query:
{
a {
y
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "subgraphA":
- cannot find field "A.y".
- cannot move to subgraph "subgraphB", which has field "A.y", because type "A" has no @key defined in subgraph "subgraphB".
`],
);
});
describe('field sharing', () => {
it ('errors if a non-shareable fields are shared in "value types"', () => {
const subgraphA = {
typeDefs: gql`
type Query {
a: A
}
type A {
x: Int
y: Int
z: Int
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type A {
x: Int
z: Int @shareable
}
`,
name: 'subgraphB',
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_FIELD_SHARING', 'Non-shareable field "A.x" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in all of them'],
['INVALID_FIELD_SHARING', 'Non-shareable field "A.z" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in subgraph "subgraphA"'],
]);
});
it ('errors if a non-shareable fields are shared in an "entity type"', () => {
const subgraphA = {
typeDefs: gql`
type Query {
a: A
}
type A @key(fields: "x") {
x: Int
y: Int
z: Int
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type A @key(fields: "x") {
x: Int
z: Int @shareable
}
`,
name: 'subgraphB',
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_FIELD_SHARING', 'Non-shareable field "A.z" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in subgraph "subgraphA"'],
]);
});
it ('errors if a query is shared without @shareable', () => {
const subgraphA = {
typeDefs: gql`
type Query {
me: String
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type Query {
me: String
}
`,
name: 'subgraphB',
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_FIELD_SHARING', 'Non-shareable field "Query.me" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in all of them'],
]);
});
it ('errors if provided fields are not marked @shareable', () => {
const subgraphA = {
typeDefs: gql`
type Query {
e: E
}
type E @key(fields: "id") {
id: ID!
a: Int
b: Int
c: Int
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type Query {
eWithProvided: E @provides(fields: "a c")
}
type E @key(fields: "id") {
id: ID!
a: Int @external
c: Int @external
d: Int
}
`,
name: 'subgraphB',
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_FIELD_SHARING', 'Non-shareable field "E.a" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in subgraph "subgraphA"'],
['INVALID_FIELD_SHARING', 'Non-shareable field "E.c" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in subgraph "subgraphA"'],
]);
});
it ('applies @shareable on type only to the field within the definition', () => {
const subgraphA = {
typeDefs: gql`
type Query {
e: E
}
type E @shareable {
id: ID!
a: Int
}
extend type E {
b: Int
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
type E @shareable {
id: ID!
a: Int
b: Int
}
`,
name: 'subgraphB',
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
// We want the @shareable to only apply to `a` but not `b` in the first
// subgraph, so this should _not_ compose.
expect(result.errors).toBeDefined();
expect(errors(result)).toStrictEqual([
['INVALID_FIELD_SHARING', 'Non-shareable field "E.b" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" and "subgraphB" and defined as non-shareable in subgraph "subgraphA"'],
]);
});
});
it('handles renamed federation directives', () => {
const subgraphA = {
typeDefs: gql`
extend schema @link(
url: "https://specs.apollo.dev/federation/v2.0",
import: [{ name: "@key", as: "@identity"}, {name: "@requires", as: "@gimme"}, {name: "@external", as: "@notInThisSubgraph"}]
)
type Query {
users: [User]
}
type User @identity(fields: "id") {
id: ID!
name: String!
birthdate: String! @notInThisSubgraph
age: Int! @gimme(fields: "birthdate")
}
`,
name: 'subgraphA',
};
const subgraphB = {
typeDefs: gql`
extend schema @link(
url: "https://specs.apollo.dev/federation/v2.0",
import: [{ name: "@key", as: "@myKey"}]
)
type User @myKey(fields: "id") {
id: ID!
birthdate: String!
}
`,
name: 'subgraphB',
};
// Note that we don't use `composeAsFed2Subgraph` since we @link manually in that example.
const result = composeServices([subgraphA, subgraphB]);
assertCompositionSuccess(result);
const [supergraph, api] = schemas(result);
expect(printSchema(api)).toMatchString(`
type Query {
users: [User]
}
type User {
id: ID!
name: String!
birthdate: String!
age: Int!
}
`);
/*
* We validate that all the directives have been properly processed, namely:
* - That `User` has a key in both subgraphs
* - That `User.birthdate` is external in the first subgraph.
* - That `User.age` does require `birthdate`.
*/
expect(printSchema(supergraph)).toMatchString(`
schema
@core(feature: \"https://specs.apollo.dev/core/v0.2\")
@core(feature: \"https://specs.apollo.dev/join/v0.2\", for: EXECUTION)
{
query: Query
}
directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
enum core__Purpose {
"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY
"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION
}
scalar join__FieldSet
enum join__Graph {
SUBGRAPHA @join__graph(name: "subgraphA", url: "")
SUBGRAPHB @join__graph(name: "subgraphB", url: "")
}
type Query
@join__type(graph: SUBGRAPHA)
@join__type(graph: SUBGRAPHB)
{
users: [User] @join__field(graph: SUBGRAPHA)
}
type User
@join__type(graph: SUBGRAPHA, key: "id")
@join__type(graph: SUBGRAPHB, key: "id")
{
id: ID!
name: String! @join__field(graph: SUBGRAPHA)
birthdate: String! @join__field(graph: SUBGRAPHA, external: true) @join__field(graph: SUBGRAPHB)
age: Int! @join__field(graph: SUBGRAPHA, requires: "birthdate")
}
`);
})
});

@@ -0,3 +1,3 @@

import { asFed2SubgraphDocument, buildSubgraph, Subgraphs } from '@apollo/federation-internals';
import { DocumentNode } from 'graphql';
import { Subgraphs } from '@apollo/federation-internals';
import gql from 'graphql-tag';

@@ -32,3 +32,3 @@ import {

try {
subgraphs.add(name, `https://${name}`, doc);
subgraphs.add(buildSubgraph(name, `https://${name}`, asFed2SubgraphDocument(doc)));
} catch (e) {

@@ -91,3 +91,3 @@ throw new Error(e.toString());

type T {
type T @shareable {
f: String

@@ -98,3 +98,3 @@ }

const subgraph2 = gql`
type T {
type T @shareable {
f: String!

@@ -122,3 +122,3 @@ }

type T {
type T @shareable {
f: I

@@ -137,3 +137,3 @@ }

type T {
type T @shareable {
f: Impl

@@ -157,3 +157,3 @@ }

type T {
type T @shareable {
f(a: String!): String

@@ -164,3 +164,3 @@ }

const subgraph2 = gql`
type T {
type T @shareable {
f(a: String): String

@@ -184,3 +184,3 @@ }

type T {
type T @shareable {
f(a: String = "foo"): String

@@ -191,3 +191,3 @@ }

const subgraph2 = gql`
type T {
type T @shareable {
f(a: String): String

@@ -218,3 +218,3 @@ }

const subgraph2 = gql`
type T {
type T @shareable {
k: Int

@@ -239,3 +239,3 @@ v2: Int

type T {
type T @shareable {
a: Int

@@ -247,3 +247,3 @@ b: Int

const subgraph2 = gql`
type T {
type T @shareable {
a: Int

@@ -321,3 +321,3 @@ }

type A {
type A @shareable {
a: Int

@@ -330,3 +330,3 @@ }

type C {
type C @shareable {
b: Int

@@ -339,7 +339,7 @@ }

type A {
type A @shareable {
a: Int
}
type C {
type C @shareable {
b: Int

@@ -383,6 +383,5 @@ }

test('hints on type system directives having inconsistent repeatable', () => {
// Note that the code currently only merge a handful of hard-coded type system directive, so we have
// to use of the known names. We use 'tag'.
// Skipped for now because we don't merge any type system directives and so
// this cannot be properly tested.
test.skip('hints on type system directives having inconsistent repeatable', () => {
const subgraph1 = gql`

@@ -408,3 +407,5 @@ type Query {

test('hints on type system directives having inconsistent locations', () => {
// Skipped for now because we don't merge any type system directives and so
// this cannot be properly tested.
test.skip('hints on type system directives having inconsistent locations', () => {
// Same as above, we kind of have to use tag.

@@ -589,3 +590,3 @@ const subgraph1 = gql`

type T {
type T @shareable {
"I don't know what I'm doing"

@@ -597,3 +598,3 @@ f: Int

const subgraph2 = gql`
type T {
type T @shareable {
"Return a super secret integer"

@@ -605,3 +606,3 @@ f: Int

const subgraph3 = gql`
type T {
type T @shareable {
"""

@@ -608,0 +609,0 @@ Return a super secret integer

@@ -1,4 +0,5 @@

import { CompositionResult, composeServices } from '../compose';
import { CompositionResult } from '../compose';
import gql from 'graphql-tag';
import './matchers';
import { composeAsFed2Subgraphs } from './compose.test';

@@ -37,3 +38,3 @@ function errorMessages(r: CompositionResult): string[] {

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -85,3 +86,3 @@ expect(errorMessages(result)).toMatchStringArray([

type T1 {
id: Int!
id: Int! @shareable
f1: String @external

@@ -97,3 +98,3 @@ f2: T2! @requires(fields: "f1")

const result = composeServices([subgraphA, subgraphB]);
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();

@@ -116,3 +117,48 @@ expect(errorMessages(result)).toMatchStringArray([

});
});
describe('non-resolvable keys', () => {
it('fails if key is declared non-resolvable but would be needed', () => {
global.console = require('console');
const subgraphA = {
name: 'A',
typeDefs: gql`
type T @key(fields: "id", resolvable: false) {
id: ID!
f: String
}
`
};
const subgraphB = {
name: 'B',
typeDefs: gql`
type Query {
getTs: [T]
}
type T @key(fields: "id") {
id: ID!
}
`
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
expect(result.errors).toBeDefined();
expect(errorMessages(result)).toMatchStringArray([
`
The following supergraph API query:
{
getTs {
f
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "B":
- cannot find field "T.f".
- cannot move to subgraph "A", which has field "T.f", because none of the @key defined on type "T" in subgraph "A" are resolvable (they are all declared with their "resolvable" argument set to false).
`
]);
});
});

@@ -10,2 +10,3 @@ import {

ERRORS,
upgradeSubgraphsIfNecessary,
} from "@apollo/federation-internals";

@@ -35,3 +36,14 @@ import { GraphQLError } from "graphql";

export function compose(subgraphs: Subgraphs): CompositionResult {
const mergeResult = mergeSubgraphs(subgraphs);
const upgradeResult = upgradeSubgraphsIfNecessary(subgraphs);
if (upgradeResult.errors) {
return { errors: upgradeResult.errors };
}
const toMerge = upgradeResult.subgraphs;
const validationErrors = toMerge.validate();
if (validationErrors) {
return { errors: validationErrors };
}
const mergeResult = mergeSubgraphs(toMerge);
if (mergeResult.errors) {

@@ -38,0 +50,0 @@ return { errors: mergeResult.errors };

@@ -118,8 +118,6 @@ import { SubgraphASTNode } from "@apollo/federation-internals";

// Note that we keep the hint somewhat generic, but it is currently only used for execution directive so we specify
// this in the description for clarity.
export const hintInconsistentArgumentPresence = new HintID(
'InconsistentArgumentPresence',
'Indicates that an argument of an execution directive definition is not present in all subgraphs '
+ 'and will not be part of the supergraph',
'Indicates that an (optional) argument (of a field or directive definition) is not present in all subgraphs '
+ ' and will not be part of the supergraph',
'the argument with mismatched types'

@@ -126,0 +124,0 @@ );

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 too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc