Skip to content

Commit

Permalink
improve connect merging (#3118)
Browse files Browse the repository at this point in the history
* reusing isMergedType for removing connect types
* add the link directive to the supergraph with its purpose to block
router 1.x from executing these supergraphs
* use createScalarTypeSpecification()
* remove unused types

<!-- https://apollographql.atlassian.net/browse/CNN-396
https://apollographql.atlassian.net/browse/CNN-397 -->

<!--
First, 🌠 thank you 🌠 for taking the time to consider a contribution to
Apollo!

Here are some important details to follow:

* ⏰ Your time is important
To save your precious time, if the contribution you are making will
take more than an hour, please make sure it has been discussed in an
        issue first. This is especially true for feature requests!

* 💡 Features
Feature requests can be created and discussed within a GitHub Issue.
Be sure to search for existing feature requests (and related issues!)
prior to opening a new request. If an existing issue covers the need,
please upvote that issue by using the 👍 emote, rather than opening a
        new issue.

* 🕷 Bug fixes
These can be created and discussed in this repository. When fixing a
bug,
please _try_ to add a test which verifies the fix. If you cannot, you
should
still submit the PR but we may still ask you (and help you!) to create a
test.

* Federation versions
Please make sure you're targeting the federation version you're opening
the PR for. Federation 2 (alpha) is currently located on the `main`
branch and prior versions of Federation live on the `version-0.x`
branch.

* 📖 Contribution guidelines
Follow
https://github.com/apollographql/federation/blob/HEAD/CONTRIBUTING.md
when submitting a pull request. Make sure existing tests still pass, and
add
        tests for all new behavior.

* ✏️ Explain your pull request
Describe the big picture of your changes here to communicate to what
        your pull request is meant to accomplish. Provide 🔗 links 🔗 to
        associated issues!

We hope you will find this to be a positive experience! Open source
contribution can be intimidating and we hope to alleviate that pain as
much
as possible. Without following these guidelines, you may be missing
context
that can help you succeed with your contribution, which is why we
encourage
discussion first. Ultimately, there is no guarantee that we will be able
to
merge your pull-request, but by following these guidelines we can try to
avoid disappointment.

-->
  • Loading branch information
lennyburdette authored Aug 15, 2024
1 parent d72d138 commit eee8a80
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 179 deletions.
206 changes: 104 additions & 102 deletions composition-js/src/__tests__/connectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,75 +36,76 @@ describe("connect spec and join__directive", () => {
expect(result.errors ?? []).toEqual([]);
const printed = printSchema(result.schema!);
expect(printed).toMatchInlineSnapshot(`
"schema
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
@link(url: \\"https://specs.apollo.dev/join/v0.5\\", for: EXECUTION)
@join__directive(graphs: [WITH_CONNECTORS], name: \\"link\\", args: {url: \\"https://specs.apollo.dev/connect/v0.1\\", import: [\\"@connect\\", \\"@source\\"]})
@join__directive(graphs: [WITH_CONNECTORS], name: \\"source\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
{
query: Query
}
"schema
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
@link(url: \\"https://specs.apollo.dev/join/v0.5\\", for: EXECUTION)
@link(url: \\"https://specs.apollo.dev/connect/v0.1\\", for: EXECUTION)
@join__directive(graphs: [WITH_CONNECTORS], name: \\"link\\", args: {url: \\"https://specs.apollo.dev/connect/v0.1\\", import: [\\"@connect\\", \\"@source\\"]})
@join__directive(graphs: [WITH_CONNECTORS], name: \\"source\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
{
query: Query
}
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
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__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__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
scalar link__Import
scalar link__Import
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
scalar join__FieldSet
scalar join__FieldSet
scalar join__DirectiveArguments
scalar join__DirectiveArguments
scalar join__FieldValue
scalar join__FieldValue
input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}
input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"connect\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}, selection: \\"\\"})
}
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"connect\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}, selection: \\"\\"})
}
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);

if (result.schema) {
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -157,75 +158,76 @@ describe("connect spec and join__directive", () => {
expect(result.errors ?? []).toEqual([]);
const printed = printSchema(result.schema!);
expect(printed).toMatchInlineSnapshot(`
"schema
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
@link(url: \\"https://specs.apollo.dev/join/v0.5\\", for: EXECUTION)
@join__directive(graphs: [WITH_CONNECTORS], name: \\"link\\", args: {url: \\"https://specs.apollo.dev/connect/v0.1\\", as: \\"http\\", import: [{name: \\"@connect\\", as: \\"@http\\"}, {name: \\"@source\\", as: \\"@api\\"}]})
@join__directive(graphs: [WITH_CONNECTORS], name: \\"api\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
{
query: Query
}
"schema
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
@link(url: \\"https://specs.apollo.dev/join/v0.5\\", for: EXECUTION)
@link(url: \\"https://specs.apollo.dev/connect/v0.1\\", for: EXECUTION)
@join__directive(graphs: [WITH_CONNECTORS], name: \\"link\\", args: {url: \\"https://specs.apollo.dev/connect/v0.1\\", as: \\"http\\", import: [{name: \\"@connect\\", as: \\"@http\\"}, {name: \\"@source\\", as: \\"@api\\"}]})
@join__directive(graphs: [WITH_CONNECTORS], name: \\"api\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
{
query: Query
}
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
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__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__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
scalar link__Import
scalar link__Import
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
scalar join__FieldSet
scalar join__FieldSet
scalar join__DirectiveArguments
scalar join__DirectiveArguments
scalar join__FieldValue
scalar join__FieldValue
input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}
input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"http\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}, selection: \\"\\"})
}
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"http\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}, selection: \\"\\"})
}
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);

if (result.schema) {
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
Expand Down
58 changes: 19 additions & 39 deletions composition-js/src/merging/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function copyTypeReference(source: Type, dest: Schema): Type {
}
}

const NON_MERGED_CORE_FEATURES = [ federationIdentity, linkIdentity, coreIdentity ];
const NON_MERGED_CORE_FEATURES = [ federationIdentity, linkIdentity, coreIdentity, connectIdentity ];

function isMergedType(type: NamedType): boolean {
if (type.isIntrospectionType() || FEDERATION_OPERATION_TYPES.map((s) => s.name).includes(type.name)) {
Expand Down Expand Up @@ -658,10 +658,6 @@ class Merger {
// we want to make sure everything is ready.
this.addMissingInterfaceObjectFieldsToImplementations();

// After converting some `@link`ed definitions to use `@join__directive`,
// we might have some imported scalars and input types to remove from the schema.
this.removeTypesAfterJoinDirectiveSerialization(this.merged);

// If we already encountered errors, `this.merged` is probably incomplete. Let's not risk adding errors that
// are only an artifact of that incompleteness as it's confusing.
if (this.errors.length === 0) {
Expand Down Expand Up @@ -3074,6 +3070,7 @@ class Merger {
args: Record<string, any>;
}>
} = Object.create(null);
const linksToPersist = new Set<FeatureDefinition>();

for (const [idx, source] of sources.entries()) {
if (!source) continue;
Expand All @@ -3094,7 +3091,15 @@ class Merger {
if (typeof url === 'string' && parsedUrl) {
shouldIncludeAsJoinDirective =
this.shouldUseJoinDirectiveForURL(parsedUrl);

if (shouldIncludeAsJoinDirective) {
const featureDefinition = coreFeatureDefinitionIfKnown(parsedUrl);
if (featureDefinition) {
linksToPersist.add(featureDefinition);
}
}
}

} else {
// To be consistent with other code accessing
// linkImportIdentityURLMap, we ensure directive names start with a
Expand Down Expand Up @@ -3126,6 +3131,15 @@ class Merger {
}
}

const linkDirective = this.linkSpec.coreDirective(this.merged);
for (const link of linksToPersist) {
dest.applyDirective(linkDirective, {
url: link.toString(),
for: link.defaultCorePurpose,
feature: undefined
});
}

const joinDirective = this.joinSpec.directiveDirective(this.merged);
Object.keys(joinsByDirectiveName).forEach(directiveName => {
joinsByDirectiveName[directiveName].forEach(join => {
Expand All @@ -3138,40 +3152,6 @@ class Merger {
});
}

// After merging, if we added any join__directive directives, we want to
// remove types imported from the original `@link` directive to avoid
// orphaned types. When extractSubgraphsFromSupergraph is called, it will
// add the types back to the subgraph.
private removeTypesAfterJoinDirectiveSerialization(schema: Schema) {
const joinDirectiveLinks = schema.directives()
.filter(d => d.name === 'join__directive')
.flatMap(d => Array.from(d.applications()))
.filter(a => a.arguments().name === 'link');

// We can't use `.nameInSchema()` because the `@link` directive isn't
// directly in the schema, it's obscured by the `@join__directive` directive
const joinDirectiveFieldsWithNamespaces = Object.fromEntries(joinDirectiveLinks.flatMap(link => {
const url = link.arguments().args.url;
const parsed = FeatureUrl.parse(url);
if (parsed) {
const featureDefinition = coreFeatureDefinitionIfKnown(parsed);
if (featureDefinition) {
const nameInSchema = link.arguments().args.as ?? featureDefinition.url.name;
return [[nameInSchema, featureDefinition]] as [string, FeatureDefinition][];
}
}

// TODO: error if we can't parse URLs or find core feature definitions?
return [] as [string, FeatureDefinition][];
}));

for (const [namespace, featureDefinition] of Object.entries(joinDirectiveFieldsWithNamespaces)) {
featureDefinition.allElementNames().forEach(name => {
schema.type(`${namespace}__${name}`)?.removeRecursive()
});
}
}

private filterSubgraphs(predicate: (schema: Schema) => boolean): string[] {
return this.subgraphsSchema.map((s, i) => predicate(s) ? this.names[i] : undefined).filter(n => n !== undefined) as string[];
}
Expand Down
Loading

0 comments on commit eee8a80

Please sign in to comment.