Skip to content

Commit

Permalink
feat: add connect/v0.1 spec
Browse files Browse the repository at this point in the history
replaces the source/v0.1 spec

also attempts to clean up imported types that aren't used when serializating directives with join__directive
  • Loading branch information
lennyburdette committed Apr 11, 2024
1 parent 4e5f79d commit 22defef
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 0 deletions.
222 changes: 222 additions & 0 deletions composition-js/src/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5203,3 +5203,225 @@ describe('@source* directives', () => {
});
});
});

describe("connect spec and join__directive", () => {
it("composes", () => {
const subgraphs = [
{
name: "with-connectors",
typeDefs: gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.7"
import: ["@key"]
)
@link(
url: "https://specs.apollo.dev/connect/v0.1"
import: ["@connect", "@source"]
)
@source(name: "v1", http: { baseURL: "http://v1" })
type Query {
resources: [Resource!]!
@connect(source: "v1", http: { GET: "/resources" })
}
type Resource @key(fields: "id") {
id: ID!
name: String!
}
`,
},
];

const result = composeServices(subgraphs);
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.4\\", 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 @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__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
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__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
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
scalar link__Import
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
scalar join__FieldSet
scalar join__DirectiveArguments
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"connect\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}})
}
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);

if (result.schema) {
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
"type Query {
resources: [Resource!]!
}
type Resource {
id: ID!
name: String!
}"
`);
}
});

it("composes with renames", () => {
const subgraphs = [
{
name: "with-connectors",
typeDefs: gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.7"
import: ["@key"]
)
@link(
url: "https://specs.apollo.dev/connect/v0.1"
as: "http"
import: [
{ name: "@connect", as: "@http" }
{ name: "@source", as: "@api" }
]
)
@api(name: "v1", http: { baseURL: "http://v1" })
type Query {
resources: [Resource!]!
@http(source: "v1", http: { GET: "/resources" })
}
type Resource @key(fields: "id") {
id: ID!
name: String!
}
`,
},
];

const result = composeServices(subgraphs);
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.4\\", 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 @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__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
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__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
enum link__Purpose {
\\"\\"\\"
\`SECURITY\` features provide metadata necessary to securely resolve fields.
\\"\\"\\"
SECURITY
\\"\\"\\"
\`EXECUTION\` features provide metadata necessary for operation execution.
\\"\\"\\"
EXECUTION
}
scalar link__Import
enum join__Graph {
WITH_CONNECTORS @join__graph(name: \\"with-connectors\\", url: \\"\\")
}
scalar join__FieldSet
scalar join__DirectiveArguments
type Query
@join__type(graph: WITH_CONNECTORS)
{
resources: [Resource!]! @join__directive(graphs: [WITH_CONNECTORS], name: \\"http\\", args: {source: \\"v1\\", http: {GET: \\"/resources\\"}})
}
type Resource
@join__type(graph: WITH_CONNECTORS, key: \\"id\\")
{
id: ID!
name: String!
}"
`);

if (result.schema) {
expect(printSchema(result.schema.toAPISchema())).toMatchInlineSnapshot(`
"type Query {
resources: [Resource!]!
}
type Resource {
id: ID!
name: String!
}"
`);
}
});
});
44 changes: 44 additions & 0 deletions composition-js/src/merging/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ import {
FeatureUrl,
CoreFeature,
Subgraph,
connectIdentity,
coreFeatureDefinitionIfKnown,
FeatureDefinition,
} from "@apollo/federation-internals";
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
import {
Expand Down Expand Up @@ -341,6 +344,7 @@ class Merger {
[ // Represent any applications of directives imported from these spec URLs
// using @join__directive in the merged supergraph.
sourceIdentity,
connectIdentity,
].forEach(url => this.joinDirectiveIdentityURLs.add(url));
}

Expand Down Expand Up @@ -557,6 +561,10 @@ 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 @@ -2816,6 +2824,42 @@ 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.
//
// TODO: this doesn't handle renamed imports
private removeTypesAfterJoinDirectiveSerialization(schema: Schema) {
const joinDirectiveLinks = schema.directives()
.filter(d => d.name === 'join__directive')
.flatMap(d => 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][];

Check warning on line 2853 in composition-js/src/merging/merge.ts

View check run for this annotation

Codecov / codecov/patch

composition-js/src/merging/merge.ts#L2853

Added line #L2853 was not covered by tests
}));

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
1 change: 1 addition & 0 deletions internals-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './specs/authenticatedSpec';
export * from './specs/requiresScopesSpec';
export * from './specs/policySpec';
export * from './specs/sourceSpec';
export * from './specs/connectSpec';
Loading

0 comments on commit 22defef

Please sign in to comment.