Skip to content

Commit 22defef

Browse files
committed
feat: add connect/v0.1 spec
replaces the source/v0.1 spec also attempts to clean up imported types that aren't used when serializating directives with join__directive
1 parent 4e5f79d commit 22defef

File tree

4 files changed

+413
-0
lines changed

4 files changed

+413
-0
lines changed

composition-js/src/__tests__/compose.test.ts

+222
Original file line numberDiff line numberDiff line change
@@ -5203,3 +5203,225 @@ describe('@source* directives', () => {
52035203
});
52045204
});
52055205
});
5206+
5207+
describe("connect spec and join__directive", () => {
5208+
it("composes", () => {
5209+
const subgraphs = [
5210+
{
5211+
name: "with-connectors",
5212+
typeDefs: gql`
5213+
extend schema
5214+
@link(
5215+
url: "https://specs.apollo.dev/federation/v2.7"
5216+
import: ["@key"]
5217+
)
5218+
@link(
5219+
url: "https://specs.apollo.dev/connect/v0.1"
5220+
import: ["@connect", "@source"]
5221+
)
5222+
@source(name: "v1", http: { baseURL: "http://v1" })
5223+
5224+
type Query {
5225+
resources: [Resource!]!
5226+
@connect(source: "v1", http: { GET: "/resources" })
5227+
}
5228+
5229+
type Resource @key(fields: "id") {
5230+
id: ID!
5231+
name: String!
5232+
}
5233+
`,
5234+
},
5235+
];
5236+
5237+
const result = composeServices(subgraphs);
5238+
expect(result.errors ?? []).toEqual([]);
5239+
const printed = printSchema(result.schema!);
5240+
expect(printed).toMatchInlineSnapshot(`
5241+
"schema
5242+
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
5243+
@link(url: \\"https://specs.apollo.dev/join/v0.4\\", for: EXECUTION)
5244+
@join__directive(graphs: [WITH_CONNECTORS], name: \\"link\\", args: {url: \\"https://specs.apollo.dev/connect/v0.1\\", import: [\\"@connect\\", \\"@source\\"]})
5245+
@join__directive(graphs: [WITH_CONNECTORS], name: \\"source\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
5246+
{
5247+
query: Query
5248+
}
5249+
5250+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
5251+
5252+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
5253+
5254+
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
5255+
5256+
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
5257+
5258+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
5259+
5260+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
5261+
5262+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
5263+
5264+
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
5265+
5266+
enum link__Purpose {
5267+
\\"\\"\\"
5268+
\`SECURITY\` features provide metadata necessary to securely resolve fields.
5269+
\\"\\"\\"
5270+
SECURITY
5271+
5272+
\\"\\"\\"
5273+
\`EXECUTION\` features provide metadata necessary for operation execution.
5274+
\\"\\"\\"
5275+
EXECUTION
5276+
}
5277+
5278+
scalar link__Import
5279+
5280+
enum join__Graph {
5281+
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
5282+
}
5283+
5284+
scalar join__FieldSet
5285+
5286+
scalar join__DirectiveArguments
5287+
5288+
type Query
5289+
@join__type(graph: WITH_CONNECTORS)
5290+
{
5291+
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"connect\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}})
5292+
}
5293+
5294+
type Resource
5295+
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
5296+
{
5297+
id: ID!
5298+
name: String!
5299+
}"
5300+
`);
5301+
5302+
if (result.schema) {
5303+
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
5304+
"type Query {
5305+
resources: [Resource!]!
5306+
}
5307+
5308+
type Resource {
5309+
id: ID!
5310+
name: String!
5311+
}"
5312+
`);
5313+
}
5314+
});
5315+
5316+
it("composes with renames", () => {
5317+
const subgraphs = [
5318+
{
5319+
name: "with-connectors",
5320+
typeDefs: gql`
5321+
extend schema
5322+
@link(
5323+
url: "https://specs.apollo.dev/federation/v2.7"
5324+
import: ["@key"]
5325+
)
5326+
@link(
5327+
url: "https://specs.apollo.dev/connect/v0.1"
5328+
as: "http"
5329+
import: [
5330+
{ name: "@connect", as: "@http" }
5331+
{ name: "@source", as: "@api" }
5332+
]
5333+
)
5334+
@api(name: "v1", http: { baseURL: "http://v1" })
5335+
5336+
type Query {
5337+
resources: [Resource!]!
5338+
@http(source: "v1", http: { GET: "/resources" })
5339+
}
5340+
5341+
type Resource @key(fields: "id") {
5342+
id: ID!
5343+
name: String!
5344+
}
5345+
`,
5346+
},
5347+
];
5348+
5349+
const result = composeServices(subgraphs);
5350+
expect(result.errors ?? []).toEqual([]);
5351+
const printed = printSchema(result.schema!);
5352+
expect(printed).toMatchInlineSnapshot(`
5353+
"schema
5354+
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
5355+
@link(url: \\"https://specs.apollo.dev/join/v0.4\\", for: EXECUTION)
5356+
@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\\"}]})
5357+
@join__directive(graphs: [WITH_CONNECTORS], name: \\"api\\", args: {name: \\"v1\\", http: {baseURL: \\"http://v1\\"}})
5358+
{
5359+
query: Query
5360+
}
5361+
5362+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
5363+
5364+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
5365+
5366+
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
5367+
5368+
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
5369+
5370+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
5371+
5372+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
5373+
5374+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
5375+
5376+
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
5377+
5378+
enum link__Purpose {
5379+
\\"\\"\\"
5380+
\`SECURITY\` features provide metadata necessary to securely resolve fields.
5381+
\\"\\"\\"
5382+
SECURITY
5383+
5384+
\\"\\"\\"
5385+
\`EXECUTION\` features provide metadata necessary for operation execution.
5386+
\\"\\"\\"
5387+
EXECUTION
5388+
}
5389+
5390+
scalar link__Import
5391+
5392+
enum join__Graph {
5393+
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
5394+
}
5395+
5396+
scalar join__FieldSet
5397+
5398+
scalar join__DirectiveArguments
5399+
5400+
type Query
5401+
@join__type(graph: WITH_CONNECTORS)
5402+
{
5403+
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"http\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}})
5404+
}
5405+
5406+
type Resource
5407+
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
5408+
{
5409+
id: ID!
5410+
name: String!
5411+
}"
5412+
`);
5413+
5414+
if (result.schema) {
5415+
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
5416+
"type Query {
5417+
resources: [Resource!]!
5418+
}
5419+
5420+
type Resource {
5421+
id: ID!
5422+
name: String!
5423+
}"
5424+
`);
5425+
}
5426+
});
5427+
});

composition-js/src/merging/merge.ts

+44
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ import {
7676
FeatureUrl,
7777
CoreFeature,
7878
Subgraph,
79+
connectIdentity,
80+
coreFeatureDefinitionIfKnown,
81+
FeatureDefinition,
7982
} from "@apollo/federation-internals";
8083
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
8184
import {
@@ -341,6 +344,7 @@ class Merger {
341344
[ // Represent any applications of directives imported from these spec URLs
342345
// using @join__directive in the merged supergraph.
343346
sourceIdentity,
347+
connectIdentity,
344348
].forEach(url => this.joinDirectiveIdentityURLs.add(url));
345349
}
346350

@@ -557,6 +561,10 @@ class Merger {
557561
// we want to make sure everything is ready.
558562
this.addMissingInterfaceObjectFieldsToImplementations();
559563

564+
// After converting some `@link`ed definitions to use `@join__directive`,
565+
// we might have some imported scalars and input types to remove from the schema.
566+
this.removeTypesAfterJoinDirectiveSerialization(this.merged);
567+
560568
// If we already encountered errors, `this.merged` is probably incomplete. Let's not risk adding errors that
561569
// are only an artifact of that incompleteness as it's confusing.
562570
if (this.errors.length === 0) {
@@ -2816,6 +2824,42 @@ class Merger {
28162824
});
28172825
}
28182826

2827+
// After merging, if we added any join__directive directives, we want to
2828+
// remove types imported from the original `@link` directive to avoid
2829+
// orphaned types. When extractSubgraphsFromSupergraph is called, it will
2830+
// add the types back to the subgraph.
2831+
//
2832+
// TODO: this doesn't handle renamed imports
2833+
private removeTypesAfterJoinDirectiveSerialization(schema: Schema) {
2834+
const joinDirectiveLinks = schema.directives()
2835+
.filter(d => d.name === 'join__directive')
2836+
.flatMap(d => d.applications())
2837+
.filter(a => a.arguments().name === 'link');
2838+
2839+
// We can't use `.nameInSchema()` because the `@link` directive isn't
2840+
// directly in the schema, it's obscured by the `@join__directive` directive
2841+
const joinDirectiveFieldsWithNamespaces = Object.fromEntries(joinDirectiveLinks.flatMap(link => {
2842+
const url = link.arguments().args.url;
2843+
const parsed = FeatureUrl.parse(url);
2844+
if (parsed) {
2845+
const featureDefinition = coreFeatureDefinitionIfKnown(parsed);
2846+
if (featureDefinition) {
2847+
const nameInSchema = link.arguments().args.as ?? featureDefinition.url.name;
2848+
return [[nameInSchema, featureDefinition]] as [string, FeatureDefinition][];
2849+
}
2850+
}
2851+
2852+
// TODO: error if we can't parse URLs or find core feature definitions?
2853+
return [] as [string, FeatureDefinition][];
2854+
}));
2855+
2856+
for (const [namespace, featureDefinition] of Object.entries(joinDirectiveFieldsWithNamespaces)) {
2857+
featureDefinition.allElementNames().forEach(name => {
2858+
schema.type(`${namespace}__${name}`)?.removeRecursive()
2859+
});
2860+
}
2861+
}
2862+
28192863
private filterSubgraphs(predicate: (schema: Schema) => boolean): string[] {
28202864
return this.subgraphsSchema.map((s, i) => predicate(s) ? this.names[i] : undefined).filter(n => n !== undefined) as string[];
28212865
}

internals-js/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from './specs/authenticatedSpec';
2424
export * from './specs/requiresScopesSpec';
2525
export * from './specs/policySpec';
2626
export * from './specs/sourceSpec';
27+
export * from './specs/connectSpec';

0 commit comments

Comments
 (0)