Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaggy-ghosts-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/cli': patch
---

Refactor schema composition to use `@graphql-hive/core` exported functions
5 changes: 5 additions & 0 deletions .changeset/tiny-badgers-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/core': minor
---

Add `createDevFetcher`, a factory that composes a supergraph from local or remote subgraphs, with built-in caching to avoid recomposing when resolved service SDLs are unchanged. This allows a local gateway instance to automatically watch subgraphs, compose a supergraph, and set the schema using the result, which removes the need to run a separate instance of the Hive CLI.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ docker/docker-compose.override.yml
test-results/

playwright-report
CLAUDE.md
125 changes: 125 additions & 0 deletions packages/libraries/cli/__tests__/dev.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
InvalidSupergraphResultError,
LocalSupergraphCompositionError,
RemoteSupergraphCompositionError,
SupergraphRegistryApiError,
} from '@graphql-hive/core';
import {
APIError,
InvalidCompositionResultError,
LocalCompositionError,
RemoteCompositionError,
} from '../src/helpers/errors';

const mockComposeSupergraphLocally = vi.fn();
const mockComposeSupergraphRemotely = vi.fn();

vi.mock('@graphql-hive/core', async () => {
const actual = await vi.importActual<typeof import('@graphql-hive/core')>('@graphql-hive/core');
return {
...actual,
composeSupergraphLocally: (...args: unknown[]) => mockComposeSupergraphLocally(...args),
composeSupergraphRemotely: (...args: unknown[]) => mockComposeSupergraphRemotely(...args),
};
});

const { default: Dev } = await import('../src/commands/dev');

function createDevInstance() {
const dev = Object.create(Dev.prototype) as any;
dev.logSuccess = vi.fn();
dev.log = vi.fn();
dev.logger = { info: vi.fn(), error: vi.fn(), debug: vi.fn() };
dev.config = { version: '0.0.0' };
return dev;
}

beforeEach(() => {
mockComposeSupergraphLocally.mockReset();
mockComposeSupergraphRemotely.mockReset();
});

describe('Dev.composeLocally', () => {
it('maps a LocalSupergraphCompositionError to a LocalCompositionError', async () => {
const compositionResult = { errors: [{ message: 'field conflict' }] } as any;
mockComposeSupergraphLocally.mockRejectedValue(
new LocalSupergraphCompositionError(compositionResult),
);

const dev = createDevInstance();
const onError = vi.fn();

await dev.composeLocally({ services: [], write: 'out.graphql', onError });

expect(onError).toHaveBeenCalledWith(expect.any(LocalCompositionError));
});

it('rethrows unrecognized errors', async () => {
mockComposeSupergraphLocally.mockRejectedValue(new Error('unexpected'));

const dev = createDevInstance();
const onError = vi.fn();

await expect(
dev.composeLocally({ services: [], write: 'out.graphql', onError }),
).rejects.toThrow('unexpected');
expect(onError).not.toHaveBeenCalled();
});
});

describe('Dev.compose', () => {
const baseInput = {
services: [],
registry: 'http://registry.localhost',
token: 'secret-token',
write: 'out.graphql',
unstable__forceLatest: false,
target: null,
};

it('maps a SupergraphRegistryApiError to an APIError', async () => {
mockComposeSupergraphRemotely.mockRejectedValue(new SupergraphRegistryApiError('bad request'));

const dev = createDevInstance();
const onError = vi.fn();

await dev.compose({ ...baseInput, onError });

expect(onError).toHaveBeenCalledWith(expect.any(APIError));
});

it('maps a RemoteSupergraphCompositionError to a CLI RemoteCompositionError', async () => {
mockComposeSupergraphRemotely.mockRejectedValue(
new RemoteSupergraphCompositionError([{ message: 'field conflict' }]),
);

const dev = createDevInstance();
const onError = vi.fn();

await dev.compose({ ...baseInput, onError });

expect(onError).toHaveBeenCalledWith(expect.any(RemoteCompositionError));
});

it('maps an InvalidSupergraphResultError to an InvalidCompositionResultError', async () => {
mockComposeSupergraphRemotely.mockRejectedValue(new InvalidSupergraphResultError(null));

const dev = createDevInstance();
const onError = vi.fn();

await dev.compose({ ...baseInput, onError });

expect(onError).toHaveBeenCalledWith(expect.any(InvalidCompositionResultError));
});

it('rethrows unrecognized errors', async () => {
mockComposeSupergraphRemotely.mockRejectedValue(new Error('network down'));

const dev = createDevInstance();
const onError = vi.fn();

await expect(dev.compose({ ...baseInput, onError })).rejects.toThrow('network down');
expect(onError).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion packages/libraries/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"oclif": "4.22.65",
"rimraf": "6.1.3",
"tsx": "4.19.2",
"typescript": "5.7.3"
"typescript": "5.7.3",
"vitest": "4.1.11"
},
"publishConfig": {
"access": "public"
Expand Down
12 changes: 5 additions & 7 deletions packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,13 @@ export default abstract class BaseCommand<T extends typeof Command> extends Comm
}

registryApi(registry: string, token: string) {
const requestHeaders = {
Authorization: `Bearer ${token}`,
'graphql-client-name': 'Hive CLI',
'graphql-client-version': this.config.version,
};

return graphqlRequest({
endpoint: registry,
additionalHeaders: requestHeaders,
additionalHeaders: {
Authorization: `Bearer ${token}`,
'graphql-client-name': 'Hive CLI',
'graphql-client-version': this.config.version,
},
version: this.config.version,
logger: this.logger,
});
Expand Down
Loading
Loading