2023-08-24 12:22:35 +00:00
|
|
|
import { writeFile } from 'node:fs/promises';
|
|
|
|
|
import { extname, resolve } from 'node:path';
|
|
|
|
|
import { Args, Flags } from '@oclif/core';
|
|
|
|
|
import Command from '../../base-command';
|
|
|
|
|
import { graphql } from '../../gql';
|
2023-12-06 08:57:11 +00:00
|
|
|
import { graphqlEndpoint } from '../../helpers/config';
|
2025-01-21 16:26:23 +00:00
|
|
|
import {
|
|
|
|
|
InvalidSchemaError,
|
|
|
|
|
MissingEndpointError,
|
|
|
|
|
MissingRegistryTokenError,
|
|
|
|
|
SchemaNotFoundError,
|
|
|
|
|
UnsupportedFileExtensionError,
|
|
|
|
|
} from '../../helpers/errors';
|
2025-01-10 13:31:32 +00:00
|
|
|
import { Texture } from '../../helpers/texture/texture';
|
2023-08-24 12:22:35 +00:00
|
|
|
|
|
|
|
|
const SchemaVersionForActionIdQuery = graphql(/* GraphQL */ `
|
|
|
|
|
query SchemaVersionForActionId(
|
|
|
|
|
$actionId: ID!
|
|
|
|
|
$includeSDL: Boolean!
|
|
|
|
|
$includeSupergraph: Boolean!
|
2025-01-08 12:16:55 +00:00
|
|
|
$includeSubgraphs: Boolean!
|
2023-08-24 12:22:35 +00:00
|
|
|
) {
|
|
|
|
|
schemaVersionForActionId(actionId: $actionId) {
|
|
|
|
|
id
|
|
|
|
|
valid
|
|
|
|
|
sdl @include(if: $includeSDL)
|
|
|
|
|
supergraph @include(if: $includeSupergraph)
|
2025-01-08 12:16:55 +00:00
|
|
|
schemas @include(if: $includeSubgraphs) {
|
|
|
|
|
nodes {
|
|
|
|
|
__typename
|
|
|
|
|
... on SingleSchema {
|
|
|
|
|
id
|
|
|
|
|
date
|
|
|
|
|
}
|
|
|
|
|
... on CompositeSchema {
|
|
|
|
|
id
|
|
|
|
|
date
|
|
|
|
|
url
|
|
|
|
|
service
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
total
|
|
|
|
|
}
|
2023-08-24 12:22:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`);
|
|
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
const LatestSchemaVersionQuery = graphql(/* GraphQL */ `
|
|
|
|
|
query LatestSchemaVersion(
|
|
|
|
|
$includeSDL: Boolean!
|
|
|
|
|
$includeSupergraph: Boolean!
|
|
|
|
|
$includeSubgraphs: Boolean!
|
|
|
|
|
) {
|
|
|
|
|
latestValidVersion {
|
|
|
|
|
id
|
|
|
|
|
valid
|
|
|
|
|
sdl @include(if: $includeSDL)
|
|
|
|
|
supergraph @include(if: $includeSupergraph)
|
|
|
|
|
schemas @include(if: $includeSubgraphs) {
|
|
|
|
|
nodes {
|
|
|
|
|
__typename
|
|
|
|
|
... on SingleSchema {
|
|
|
|
|
id
|
|
|
|
|
date
|
|
|
|
|
}
|
|
|
|
|
... on CompositeSchema {
|
|
|
|
|
id
|
|
|
|
|
date
|
|
|
|
|
url
|
|
|
|
|
service
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
total
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`);
|
|
|
|
|
|
2024-08-01 15:06:10 +00:00
|
|
|
export default class SchemaFetch extends Command<typeof SchemaFetch> {
|
2025-01-08 12:16:55 +00:00
|
|
|
static description = 'fetch a schema, supergraph, or list of subgraphs from the Hive API';
|
2023-08-24 12:22:35 +00:00
|
|
|
static flags = {
|
|
|
|
|
/** @deprecated */
|
|
|
|
|
registry: Flags.string({
|
|
|
|
|
description: 'registry address',
|
|
|
|
|
deprecated: {
|
|
|
|
|
message: 'use --registry.endpoint instead',
|
|
|
|
|
version: '0.21.0',
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
/** @deprecated */
|
|
|
|
|
token: Flags.string({
|
|
|
|
|
description: 'api token',
|
|
|
|
|
deprecated: {
|
|
|
|
|
message: 'use --registry.accessToken instead',
|
|
|
|
|
version: '0.21.0',
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
'registry.endpoint': Flags.string({
|
|
|
|
|
description: 'registry endpoint',
|
|
|
|
|
}),
|
|
|
|
|
'registry.accessToken': Flags.string({
|
|
|
|
|
description: 'registry access token',
|
|
|
|
|
}),
|
|
|
|
|
type: Flags.string({
|
|
|
|
|
aliases: ['T'],
|
2025-01-08 12:16:55 +00:00
|
|
|
description: 'Type to fetch (possible types: sdl, supergraph, subgraphs)',
|
2023-08-24 12:22:35 +00:00
|
|
|
}),
|
|
|
|
|
write: Flags.string({
|
|
|
|
|
aliases: ['W'],
|
|
|
|
|
description: 'Write to a file (possible extensions: .graphql, .gql, .gqls, .graphqls)',
|
|
|
|
|
}),
|
|
|
|
|
outputFile: Flags.string({
|
|
|
|
|
description: 'whether to write to a file instead of stdout',
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static args = {
|
|
|
|
|
actionId: Args.string({
|
|
|
|
|
name: 'actionId' as const,
|
|
|
|
|
description: 'action id (e.g. commit sha)',
|
|
|
|
|
hidden: false,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async run() {
|
|
|
|
|
const { flags, args } = await this.parse(SchemaFetch);
|
|
|
|
|
|
2025-01-21 16:26:23 +00:00
|
|
|
let endpoint: string, accessToken: string;
|
|
|
|
|
try {
|
|
|
|
|
endpoint = this.ensure({
|
|
|
|
|
key: 'registry.endpoint',
|
|
|
|
|
args: flags,
|
|
|
|
|
env: 'HIVE_REGISTRY',
|
|
|
|
|
legacyFlagName: 'registry',
|
|
|
|
|
defaultValue: graphqlEndpoint,
|
|
|
|
|
description: SchemaFetch.flags['registry.endpoint'].description!,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new MissingEndpointError();
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
accessToken = this.ensure({
|
|
|
|
|
key: 'registry.accessToken',
|
|
|
|
|
args: flags,
|
|
|
|
|
legacyFlagName: 'token',
|
|
|
|
|
env: 'HIVE_TOKEN',
|
|
|
|
|
description: SchemaFetch.flags['registry.accessToken'].description!,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new MissingRegistryTokenError();
|
|
|
|
|
}
|
2023-08-24 12:22:35 +00:00
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
const { actionId } = args;
|
2023-08-24 12:22:35 +00:00
|
|
|
|
|
|
|
|
const sdlType = this.ensure({
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
key: 'type',
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
args: flags,
|
|
|
|
|
defaultValue: 'sdl',
|
|
|
|
|
});
|
|
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
let schemaVersion;
|
|
|
|
|
if (actionId) {
|
|
|
|
|
const result = await this.registryApi(endpoint, accessToken).request({
|
|
|
|
|
operation: SchemaVersionForActionIdQuery,
|
|
|
|
|
variables: {
|
|
|
|
|
actionId,
|
|
|
|
|
includeSDL: sdlType === 'sdl',
|
|
|
|
|
includeSupergraph: sdlType === 'supergraph',
|
|
|
|
|
includeSubgraphs: sdlType === 'subgraphs',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
schemaVersion = result.schemaVersionForActionId;
|
|
|
|
|
} else {
|
|
|
|
|
const result = await this.registryApi(endpoint, accessToken).request({
|
|
|
|
|
operation: LatestSchemaVersionQuery,
|
|
|
|
|
variables: {
|
|
|
|
|
includeSDL: sdlType === 'sdl',
|
|
|
|
|
includeSupergraph: sdlType === 'supergraph',
|
|
|
|
|
includeSubgraphs: sdlType === 'subgraphs',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
schemaVersion = result.latestValidVersion;
|
|
|
|
|
}
|
2023-08-24 12:22:35 +00:00
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
if (schemaVersion == null) {
|
2025-01-21 16:26:23 +00:00
|
|
|
throw new SchemaNotFoundError(actionId);
|
2023-08-24 12:22:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
if (schemaVersion.valid === false) {
|
2025-01-21 16:26:23 +00:00
|
|
|
throw new InvalidSchemaError(actionId);
|
2023-08-24 12:22:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 03:16:19 +00:00
|
|
|
if (schemaVersion.schemas) {
|
|
|
|
|
const { total, nodes } = schemaVersion.schemas;
|
2025-01-10 13:31:32 +00:00
|
|
|
const tableData = [
|
2025-01-08 12:16:55 +00:00
|
|
|
['service', 'url', 'date'],
|
|
|
|
|
...nodes.map(node => [
|
|
|
|
|
/** @ts-expect-error: If service is undefined then use id. */
|
|
|
|
|
node.service ?? node.id,
|
|
|
|
|
node.__typename === 'CompositeSchema' ? node.url : 'n/a',
|
|
|
|
|
node.date as string,
|
|
|
|
|
]),
|
|
|
|
|
];
|
|
|
|
|
const stats = `subgraphs length: ${total}`;
|
2025-01-10 13:31:32 +00:00
|
|
|
const printed = `${Texture.table(tableData)}\n\r${stats}`;
|
2023-08-24 12:22:35 +00:00
|
|
|
|
2025-01-08 12:16:55 +00:00
|
|
|
if (flags.write) {
|
|
|
|
|
const filepath = resolve(process.cwd(), flags.write);
|
|
|
|
|
await writeFile(filepath, printed, 'utf8');
|
|
|
|
|
}
|
|
|
|
|
this.log(printed);
|
|
|
|
|
} else {
|
2025-01-11 03:16:19 +00:00
|
|
|
const schema = schemaVersion.sdl ?? schemaVersion.supergraph;
|
2025-01-08 12:16:55 +00:00
|
|
|
|
|
|
|
|
if (schema == null) {
|
2025-01-21 16:26:23 +00:00
|
|
|
throw new SchemaNotFoundError(actionId);
|
2025-01-08 12:16:55 +00:00
|
|
|
}
|
2023-08-24 12:22:35 +00:00
|
|
|
|
2025-01-08 12:16:55 +00:00
|
|
|
if (flags.write) {
|
|
|
|
|
const filepath = resolve(process.cwd(), flags.write);
|
|
|
|
|
switch (extname(flags.write.toLowerCase())) {
|
|
|
|
|
case '.graphql':
|
|
|
|
|
case '.gql':
|
|
|
|
|
case '.gqls':
|
|
|
|
|
case '.graphqls':
|
|
|
|
|
await writeFile(filepath, schema, 'utf8');
|
|
|
|
|
break;
|
|
|
|
|
default:
|
2025-01-23 17:08:13 +00:00
|
|
|
throw new UnsupportedFileExtensionError(flags.write, [
|
|
|
|
|
'.graphql',
|
|
|
|
|
'.gql',
|
|
|
|
|
'.gqls',
|
|
|
|
|
'.graphqls',
|
|
|
|
|
]);
|
2025-01-08 12:16:55 +00:00
|
|
|
}
|
|
|
|
|
return;
|
2023-08-24 12:22:35 +00:00
|
|
|
}
|
2025-01-08 12:16:55 +00:00
|
|
|
this.log(schema);
|
2023-08-24 12:22:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|