Skip to content

Commit

Permalink
Add more print schema ordering options (#2042)
Browse files Browse the repository at this point in the history
Allows passing printing options (for the supergraph) to  `compose`, and adds new printing options to sort all elements of a schema.

Co-authored-by: Ben <benweatherman@gmail.com>
Co-authored-by: Sylvain Lebresne <lebresene@gmail.com>
  • Loading branch information
3 people committed Apr 12, 2023
1 parent 179b460 commit 2c37050
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 29 deletions.
8 changes: 8 additions & 0 deletions .changeset/lucky-papayas-tie.md
@@ -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
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
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
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
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

0 comments on commit 2c37050

Please sign in to comment.