Skip to content

Commit

Permalink
feat: public schema from supergraph (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l authored Nov 24, 2023
1 parent b3365d0 commit 443283e
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 69 deletions.
8 changes: 8 additions & 0 deletions .changeset/curly-ants-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@theguild/federation-composition': minor
---

Remove `stripFederationFromSupergraph` in favor of `transformSupergraphToPublicSchema`.

Instead of stripping only federation specific types, `transformSupergraphToPublicSchema` yields the
public api schema as served by the gateway.
282 changes: 282 additions & 0 deletions __tests__/graphql/transform-supergraph-to-public-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import { parse, print } from 'graphql';
import { describe, expect, test } from 'vitest';
import { transformSupergraphToPublicSchema } from '../../src/graphql/transform-supergraph-to-public-schema';

describe('transformSupergraphToPublicSchema', () => {
describe('@inaccessible', () => {
test('scalar removal', () => {
const sdl = parse(/* GraphQL */ `
scalar Scalar1 @inaccessible
scalar Scalar2
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot('"scalar Scalar2"');
});
test('enum removal', () => {
const sdl = parse(/* GraphQL */ `
enum Enum1 @inaccessible {
VALUE1
VALUE2
}
enum Enum2 {
VALUE1
VALUE2
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"enum Enum2 {
VALUE1
VALUE2
}"
`);
});
test('object type removal', () => {
const sdl = parse(/* GraphQL */ `
type Object1 @inaccessible {
field1: String
}
type Object2 {
field1: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"type Object2 {
field1: String
}"
`);
});
test('object field removal', () => {
const sdl = parse(/* GraphQL */ `
type Object {
field1: String @inaccessible
field2: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"type Object {
field2: String
}"
`);
});
test('interface type removal', () => {
const sdl = parse(/* GraphQL */ `
interface Interface1 @inaccessible {
field1: String
}
interface Interface2 {
field1: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"interface Interface2 {
field1: String
}"
`);
});
test('interface field removal', () => {
const sdl = parse(/* GraphQL */ `
interface Interface {
field1: String @inaccessible
field2: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"interface Interface {
field2: String
}"
`);
});
test('union type removal', () => {
const sdl = parse(/* GraphQL */ `
union Union1 @inaccessible = Type1 | Type2
union Union2 = Type1 | Type2
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot('"union Union2 = Type1 | Type2"');
});
test('object field argument removal', () => {
const sdl = parse(/* GraphQL */ `
type Object {
field1(arg1: String @inaccessible): String
field2(arg1: String): String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"type Object {
field1: String
field2(arg1: String): String
}"
`);
});
test('interface field argument removal', () => {
const sdl = parse(/* GraphQL */ `
interface Object {
field1(arg1: String @inaccessible): String
field2(arg1: String): String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"interface Object {
field1: String
field2(arg1: String): String
}"
`);
});
test('input object type removal', () => {
const sdl = parse(/* GraphQL */ `
input Input1 @inaccessible {
field1: String
}
input Input2 {
field1: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"input Input2 {
field1: String
}"
`);
});
test('input object field removal', () => {
const sdl = parse(/* GraphQL */ `
input Input {
field1: String @inaccessible
field2: String
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"input Input {
field2: String
}"
`);
});
});
test('strips out all federation directives and types', () => {
const sdl = parse(/* GraphQL */ `
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
@link(
url: "https://specs.apollo.dev/inaccessible/v0.2"
as: "federation__inaccessible"
for: SECURITY
) {
query: Query
}
directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
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
scalar join__FieldSet
enum join__Graph {
BRRT @join__graph(name: "brrt", url: "http://localhost/graphql")
BUBUBU @join__graph(name: "bububu", url: "http://localhost:1/graphql")
}
scalar link__Import
enum link__Purpose {
SECURITY
EXECUTION
}
type Query @join__type(graph: BRRT) @join__type(graph: BUBUBU) {
foo: Int! @join__field(graph: BRRT)
ok1: Int! @join__field(graph: BRRT)
a: String! @join__field(graph: BUBUBU)
oi: Type2 @federation__inaccessible @join__field(graph: BUBUBU)
}
type Type2
@join__type(graph: BRRT, key: "id", extension: true)
@join__type(graph: BUBUBU, key: "id") {
id: ID! @federation__inaccessible
inStock: Boolean! @join__field(graph: BRRT)
field1: String! @federation__inaccessible @join__field(graph: BUBUBU)
}
`);

const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"type Query {
foo: Int!
ok1: Int!
a: String!
}
type Type2 {
inStock: Boolean!
}"
`);
});
test('graphql specification directives are omitted from the SDL', () => {
const sdl = parse(/* GraphQL */ `
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot('""');
});
test('does not omit @deprecated directive', () => {
const sdl = parse(/* GraphQL */ `
type Query {
foo: String @deprecated(reason: "jooo")
}
`);
const resultSdl = transformSupergraphToPublicSchema(sdl);
expect(print(resultSdl)).toMatchInlineSnapshot(`
"type Query {
foo: String @deprecated(reason: \\"jooo\\")
}"
`);
});
});
69 changes: 1 addition & 68 deletions src/graphql/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,5 @@
import {
DefinitionNode,
DirectiveDefinitionNode,
DocumentNode,
Kind,
specifiedDirectives,
visit,
} from 'graphql';
import { Kind, type DefinitionNode, type DirectiveDefinitionNode } from 'graphql';

export function isDirectiveDefinition(node: DefinitionNode): node is DirectiveDefinitionNode {
return node.kind === Kind.DIRECTIVE_DEFINITION;
}

export function stripFederationFromSupergraph(supergraph: DocumentNode) {
function removeDirective(node: {
name: {
value: string;
};
}) {
const directiveName = node.name.value;
const isSpecifiedDirective = specifiedDirectives.some(d => d.name === directiveName);
if (!isSpecifiedDirective) {
const isFederationDirective =
directiveName === 'link' ||
directiveName === 'inaccessible' ||
directiveName === 'tag' ||
directiveName === 'join__graph' ||
directiveName === 'join__type' ||
directiveName === 'join__implements' ||
directiveName === 'join__unionMember' ||
directiveName === 'join__enumValue' ||
directiveName === 'join__field';

if (isFederationDirective) {
return null;
}
}
}

return visit(supergraph, {
DirectiveDefinition: removeDirective,
Directive: removeDirective,
SchemaDefinition() {
return null;
},
SchemaExtension() {
return null;
},
EnumTypeDefinition: node => {
if (
node.name.value === 'core__Purpose' ||
node.name.value === 'join__Graph' ||
node.name.value === 'link__Purpose'
) {
return null;
}

return node;
},
ScalarTypeDefinition: node => {
if (
node.name.value === '_FieldSet' ||
node.name.value === 'link__Import' ||
node.name.value === 'join__FieldSet'
) {
return null;
}

return node;
},
});
}
Loading

0 comments on commit 443283e

Please sign in to comment.