Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more print schema ordering options #2042

Merged
8 changes: 8 additions & 0 deletions .changeset/lucky-papayas-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@apollo/composition": patch
"@apollo/federation-internals": patch
---

Allow passing print options to the `compose` method to impact how the supergraph is printed, and adds new printing
options to order all elements of the schema.

152 changes: 151 additions & 1 deletion composition-js/src/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import {
asFed2SubgraphDocument,
assert,
buildSubgraph,
defaultPrintOptions,
FEDERATION2_LINK_WITH_FULL_IMPORTS,
inaccessibleIdentity,
InputObjectType,
isObjectType,
ObjectType,
orderPrintedDefinitions,
printSchema,
printType,
} from '@apollo/federation-internals';
import { CompositionResult, composeServices } from '../compose';
import { CompositionOptions, CompositionResult, composeServices } from '../compose';
import gql from 'graphql-tag';
import './matchers';
import { print } from 'graphql';
Expand Down Expand Up @@ -168,6 +170,154 @@ describe('composition', () => {
`);
})

it('respects given compose options', () => {
const subgraph1 = {
name: 'Subgraph1',
url: 'https://Subgraph1',
typeDefs: gql`
type Query {
t: T
}

type T @key(fields: "k") {
k: ID
}

type S {
x: Int
}

union U = S | T
`
}

const subgraph2 = {
name: 'Subgraph2',
url: 'https://Subgraph2',
typeDefs: gql`
type T @key(fields: "k") {
k: ID
a: Int
b: String
}

enum E {
V1
V2
}
`
}

const options: CompositionOptions = {
sdlPrintOptions: orderPrintedDefinitions(defaultPrintOptions),
}
const result = composeAsFed2Subgraphs([subgraph1, subgraph2], options);
assertCompositionSuccess(result);

expect(result.supergraphSdl).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: 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, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

enum E
@join__type(graph: SUBGRAPH2)
{
V1 @join__enumValue(graph: SUBGRAPH2)
V2 @join__enumValue(graph: SUBGRAPH2)
}

scalar join__FieldSet

enum join__Graph {
SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1")
SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2")
}

scalar link__Import

enum link__Purpose {
"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION

"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY
}

type Query
@join__type(graph: SUBGRAPH1)
@join__type(graph: SUBGRAPH2)
{
t: T @join__field(graph: SUBGRAPH1)
}

type S
@join__type(graph: SUBGRAPH1)
{
x: Int
}

type T
@join__type(graph: SUBGRAPH1, key: "k")
@join__type(graph: SUBGRAPH2, key: "k")
{
a: Int @join__field(graph: SUBGRAPH2)
b: String @join__field(graph: SUBGRAPH2)
k: ID
}

union U
@join__type(graph: SUBGRAPH1)
@join__unionMember(graph: SUBGRAPH1, member: "S")
@join__unionMember(graph: SUBGRAPH1, member: "T")
= S | T
`);

const [_, api] = schemas(result);
expect(printSchema(api, orderPrintedDefinitions(defaultPrintOptions))).toMatchString(`
enum E {
V1
V2
}

type Query {
t: T
}

type S {
x: Int
}

type T {
a: Int
b: String
k: ID
}

union U = S | T
`);
})

it('preserves descriptions', () => {
const subgraph1 = {
name: 'Subgraph1',
Expand Down
6 changes: 3 additions & 3 deletions composition-js/src/__tests__/testHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ServiceDefinition,
Subgraphs
} from '@apollo/federation-internals';
import { CompositionResult, composeServices, CompositionSuccess } from '../compose';
import { CompositionResult, composeServices, CompositionSuccess, CompositionOptions } from '../compose';

export function assertCompositionSuccess(r: CompositionResult): asserts r is CompositionSuccess {
if (r.errors) {
Expand All @@ -28,8 +28,8 @@ export function schemas(result: CompositionSuccess): [Schema, Schema, Subgraphs]

// 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) => asFed2Service(s)));
export function composeAsFed2Subgraphs(services: ServiceDefinition[], options: CompositionOptions = {}): CompositionResult {
return composeServices(services.map((s) => asFed2Service(s)), options);
}

export function asFed2Service(service: ServiceDefinition): ServiceDefinition {
Expand Down
28 changes: 23 additions & 5 deletions composition-js/src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
Schema,
Subgraphs,
defaultPrintOptions,
orderPrintedDefinitions,
shallowOrderPrintedDefinitions,
PrintOptions,
ServiceDefinition,
subgraphsFromServiceList,
upgradeSubgraphsIfNecessary,
SubtypingRule,
assert,
} from "@apollo/federation-internals";
import { GraphQLError } from "graphql";
import { buildFederatedQueryGraph, buildSupergraphAPIQueryGraph } from "@apollo/query-graphs";
Expand All @@ -30,7 +33,22 @@ export interface CompositionSuccess {
errors?: undefined;
}

export function compose(subgraphs: Subgraphs): CompositionResult {
export interface CompositionOptions {
sdlPrintOptions?: PrintOptions;


allowedFieldTypeMergingSubtypingRules?: SubtypingRule[]
}

function validateCompositionOptions(options: CompositionOptions) {
// TODO: we currently cannot allow "list upgrades", meaning a subgraph returning `String` and another returning `[String]`. To support it, we would need the execution code to
// recognize situation and "coerce" results from the first subgraph (the one returning `String`) into singleton lists.
assert(!options?.allowedFieldTypeMergingSubtypingRules?.includes("list_upgrade"), "The `list_upgrade` field subtyping rule is currently not supported");
}

export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}): CompositionResult {
validateCompositionOptions(options);

const upgradeResult = upgradeSubgraphsIfNecessary(subgraphs);
if (upgradeResult.errors) {
return { errors: upgradeResult.errors };
Expand Down Expand Up @@ -60,7 +78,7 @@ export function compose(subgraphs: Subgraphs): CompositionResult {
try {
supergraphSdl = printSchema(
supergraphSchema,
orderPrintedDefinitions(defaultPrintOptions)
options.sdlPrintOptions ?? shallowOrderPrintedDefinitions(defaultPrintOptions),
);
} catch (err) {
return { errors: [err] };
Expand All @@ -73,13 +91,13 @@ export function compose(subgraphs: Subgraphs): CompositionResult {
};
}

export function composeServices(services: ServiceDefinition[]): CompositionResult {
export function composeServices(services: ServiceDefinition[], options: CompositionOptions = {}): CompositionResult {
const subgraphs = subgraphsFromServiceList(services);
if (Array.isArray(subgraphs)) {
// Errors in subgraphs are not truly "composition" errors, but it's probably still the best place
// to surface them in this case. Not that `subgraphsFromServiceList` do ensure the errors will
// include the subgraph name in their message.
return { errors: subgraphs };
}
return compose(subgraphs);
return compose(subgraphs, options);
}
18 changes: 2 additions & 16 deletions composition-js/src/merging/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
UnionType,
sameType,
isStrictSubtype,
SubtypingRule,
ListType,
NonNullType,
Type,
Expand All @@ -40,7 +39,6 @@ import {
addSubgraphToASTNode,
firstOf,
Extension,
DEFAULT_SUBTYPING_RULES,
isInterfaceType,
sourceASTs,
ERRORS,
Expand Down Expand Up @@ -79,6 +77,7 @@ import { ComposeDirectiveManager } from '../composeDirectiveManager';
import { MismatchReporter } from './reporter';
import { inspect } from "util";
import { collectCoreDirectivesToCompose, CoreDirectiveInSubgraphs } from "./coreDirectiveCollector";
import { CompositionOptions } from "../compose";


const linkSpec = LINK_VERSIONS.latest();
Expand All @@ -89,11 +88,6 @@ const inaccessibleSpec = INACCESSIBLE_VERSIONS.latest();

export type MergeResult = MergeSuccess | MergeFailure;

// TODO: move somewhere else.
export type CompositionOptions = {
allowedFieldTypeMergingSubtypingRules?: SubtypingRule[]
}

type FieldMergeContextProperties = {
usedOverridden: boolean,
unusedOverridden: boolean,
Expand Down Expand Up @@ -137,14 +131,6 @@ class FieldMergeContext {
}
}

// TODO:" we currently cannot allow "list upgrades", meaning a subgraph returning `String`
// and another returning `[String]`. To support it, we would need the execution code to
// recognize situation and "coerce" results from the first subgraph (the one returning
// `String`) into singleton lists.
const defaultCompositionOptions: CompositionOptions = {
allowedFieldTypeMergingSubtypingRules: DEFAULT_SUBTYPING_RULES
}

export interface MergeSuccess {
supergraph: Schema;
hints: CompositionHint[];
Expand All @@ -167,7 +153,7 @@ export function isMergeFailure(mergeResult: MergeResult): mergeResult is MergeFa

export function mergeSubgraphs(subgraphs: Subgraphs, options: CompositionOptions = {}): MergeResult {
assert(subgraphs.values().every((s) => s.isFed2Subgraph()), 'Merging should only be applied to federation 2 subgraphs');
return new Merger(subgraphs, { ...defaultCompositionOptions, ...options }).merge();
return new Merger(subgraphs, options).merge();
}

function copyTypeReference(source: Type, dest: Schema): Type {
Expand Down
Loading