Skip to content

Commit

Permalink
Add sortSDL function (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Dec 13, 2023
1 parent 284c981 commit 2d72e03
Show file tree
Hide file tree
Showing 13 changed files with 525 additions and 260 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-rules-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theguild/federation-composition': minor
---

Add sortSDL function to sort DocumentNode (type system definitions and extensions)
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
uses: the-guild-org/shared-config/.github/workflows/release-stable.yml@main
with:
releaseScript: release
nodeVersion: 20
nodeVersion: 21
packageManager: 'pnpm'
secrets:
githubToken: ${{ secrets.GUILD_BOT_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 21
cache: 'pnpm'
- name: install dependencies
run: pnpm install --frozen-lockfile
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
21
169 changes: 165 additions & 4 deletions __tests__/composition.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parse, print } from 'graphql';
import { describe, expect, test } from 'vitest';
import { sortSDL } from '../src/graphql/sort-sdl.js';
import { sdl as joinSDL } from '../src/specifications/join.js';
import { sdl as linkSDL } from '../src/specifications/link.js';
import { directive as tagDirective } from '../src/specifications/tag.js';
Expand All @@ -8,10 +9,9 @@ import {
assertCompositionSuccess,
testImplementations,
} from './shared/testkit.js';
import { normalizeAst } from './shared/utils.js';

expect.addSnapshotSerializer({
serialize: value => print(normalizeAst(parse(value as string))),
serialize: value => print(sortSDL(parse(value as string))),
test: value => typeof value === 'string' && value.includes('specs.apollo.dev'),
});

Expand All @@ -24,6 +24,43 @@ testImplementations(api => {
console.log(result.supergraphSdl);
}

test('duplicated Query fields', () => {
const result = composeServices([
{
name: 'a',
typeDefs: parse(/* GraphQL */ `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type User @key(fields: "id") {
id: ID!
name: String
}
type Query {
userById(id: ID!): User
}
`),
},
{
name: 'b',
typeDefs: parse(/* GraphQL */ `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type User @key(fields: "id") {
id: ID!
name: String
}
type Query {
userById(id: ID!): User
}
`),
},
]);

assertCompositionFailure(result);
});

describe.each(['v2.0', 'v2.1', 'v2.2', 'v2.3'] as const)('%s', version => {
describe('shareable', () => {
test('merge two exact same types', () => {
Expand Down Expand Up @@ -323,6 +360,41 @@ testImplementations(api => {
`);
});

test('merge an argument (non-nullable vs missing)', () => {
const result = composeServices([
{
name: 'a',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable"])
type Building @shareable {
# Argument is required
height(units: String!): Int!
}
type Query {
building: Building
}
`),
},
{
name: 'b',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable"])
type Building @shareable {
# Argument is missing
height: Int!
}
`),
},
]);

assertCompositionFailure(result);
});

test('merge an argument (nullable vs non-nullable)', () => {
const result = composeServices([
{
Expand Down Expand Up @@ -539,7 +611,7 @@ testImplementations(api => {
`);
});

test('merge union types with different fields', () => {
test('merge union types with different members (some are overlapping)', () => {
const result = composeServices([
{
name: 'a',
Expand Down Expand Up @@ -602,6 +674,64 @@ testImplementations(api => {
`);
});

test('merge union types with different members', () => {
const result = composeServices([
{
name: 'a',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
union Media = Book
type Book @shareable {
title: String!
}
`),
},
{
name: 'b',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
age: Int!
}
union Media = Movie
type Movie {
title: String!
}
type Query {
user: User
}
`),
},
]);

assertCompositionSuccess(result);

expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
union Media
@join__type(graph: A)
@join__type(graph: B)
@join__unionMember(graph: A, member: "Book")
@join__unionMember(graph: B, member: "Movie") =
Book
| Movie
`);
});

test('merge input types and field arguments', () => {
const result = composeServices([
{
Expand Down Expand Up @@ -2155,7 +2285,7 @@ testImplementations(api => {
`);
});

test.skipIf(api.library === 'guild')('@interfaceObject', () => {
test('@interfaceObject', () => {
const result = composeServices([
{
name: 'a',
Expand Down Expand Up @@ -2208,6 +2338,37 @@ testImplementations(api => {

if (version !== 'v2.3') {
assertCompositionFailure(result);
if (api.library === 'apollo') {
console.log(JSON.stringify(result.errors));
}
expect(result.errors).toContainEqual(
expect.objectContaining({
message: '[a] Cannot import unknown element "@interfaceObject".',
extensions: expect.objectContaining({
code: 'INVALID_LINK_DIRECTIVE_USAGE',
}),
}),
);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: '[b] Cannot import unknown element "@interfaceObject".',
extensions: expect.objectContaining({
code: 'INVALID_LINK_DIRECTIVE_USAGE',
}),
}),
);

return;
}

if (api.library === 'guild') {
// TODO: we don't support @interfaceObject yet
assertCompositionFailure(result);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: expect.stringContaining('@interfaceObject is not yet supported'),
}),
);
return;
}

Expand Down
10 changes: 5 additions & 5 deletions __tests__/shared/setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DocumentNode, TypeSystemDefinitionNode } from 'graphql';
import { Kind, parse, print } from 'graphql';
import { expect } from 'vitest';
import { normalizeAst } from './utils.js';
import { sortSDL } from '../../src/graphql/sort-sdl.js';

function isStringOrNode(value: unknown): value is string | DocumentNode | TypeSystemDefinitionNode {
return typeof value === 'string' || (!!value && typeof value === 'object' && 'kind' in value);
Expand Down Expand Up @@ -39,8 +39,8 @@ expect.extend({
}

const printed = {
received: print(normalizeAst(ensureDocumentNode(received))),
expected: print(normalizeAst(ensureDocumentNode(expected))),
received: print(sortSDL(ensureDocumentNode(received))),
expected: print(sortSDL(ensureDocumentNode(expected))),
};

if (printed.received !== printed.expected) {
Expand Down Expand Up @@ -75,8 +75,8 @@ expect.extend({
}

const printed = {
received: print(normalizeAst(ensureDocumentNode(received))),
expected: print(normalizeAst(ensureDocumentNode(expected))),
received: print(sortSDL(ensureDocumentNode(received))),
expected: print(sortSDL(ensureDocumentNode(expected))),
};

if (!printed.received.includes(printed.expected)) {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/shared/testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
CompositionResult,
composeServices as guildComposeServices,
} from '../../src/compose.js';
import { graphql, inspect } from './utils.js';
import { graphql } from './utils.js';

const missingErrorCodes = [
'DISALLOWED_INACCESSIBLE',
Expand Down Expand Up @@ -61,7 +61,7 @@ function composeServicesFactory(
const todoCodes = Array.from(uniqueCodes).filter(c => missingErrorCodes.includes(c as any));

if (todoCodes.length) {
throw new Error(['Detected', todoCodes.join(', '), 'in a test'].join(' '));
console.warn(['Detected', todoCodes.join(', '), 'in a test'].join(' '));
}
}
}
Expand Down
Loading

0 comments on commit 2d72e03

Please sign in to comment.