@graphql-hive/cli
Version:
A CLI util to manage and control your GraphQL Hive
333 lines • 14.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const graphql_1 = require("graphql");
const core_1 = require("@graphql-hive/core");
const core_2 = require("@oclif/core");
const base_command_1 = tslib_1.__importDefault(require("../../base-command"));
const gql_1 = require("../../gql");
const graphql_2 = require("../../gql/graphql");
const app_operations_1 = require("../../helpers/app-operations");
const config_1 = require("../../helpers/config");
const errors_1 = require("../../helpers/errors");
const TargetInput = tslib_1.__importStar(require("../../helpers/target-input"));
const publish_1 = require("./publish");
const FetchLatestVersionSDLQuery = (0, gql_1.graphql)(/* GraphQL */ `
query CLI_App_Create_FetchLatestVersionSDLQuery($target: TargetReferenceInput) {
latestValidVersion(target: $target) {
sdl
}
}
`);
class AppCreate extends base_command_1.default {
async run() {
var _a, _b;
const { flags, args } = await this.parse(AppCreate);
let endpoint, accessToken;
try {
endpoint = this.ensure({
key: 'registry.endpoint',
args: flags,
defaultValue: config_1.graphqlEndpoint,
env: 'HIVE_REGISTRY',
description: AppCreate.flags['registry.endpoint'].description,
});
}
catch (e) {
throw e instanceof errors_1.MissingArgumentsError ? new errors_1.MissingEndpointError() : e;
}
try {
accessToken = this.ensure({
key: 'registry.accessToken',
args: flags,
env: 'HIVE_TOKEN',
description: AppCreate.flags['registry.accessToken'].description,
});
}
catch (e) {
throw e instanceof errors_1.MissingArgumentsError ? new errors_1.MissingRegistryTokenError() : e;
}
let target = null;
if (flags.target) {
const result = TargetInput.parse(flags.target);
if (result.type === 'error') {
throw new errors_1.InvalidTargetError();
}
target = result.data;
}
const version = (_a = flags.version) !== null && _a !== void 0 ? _a : Math.random().toString(36).padEnd(9, '0').slice(2, 9);
if (!flags.version) {
this.log(`No version provided, using generated version: ${version}`);
}
const { manifest, operations, generatedFrom, warnings } = await (0, app_operations_1.loadAppOperations)(args.operations);
for (const warning of warnings) {
this.warn(warning);
}
if (generatedFrom) {
this.log(`Persisted documents manifest generated in-memory from discovered GraphQL operations under "${generatedFrom}".`);
this.log(JSON.stringify(manifest, null, 2));
}
const schemaResult = await this.registryApi(endpoint, accessToken).request({
operation: FetchLatestVersionSDLQuery,
variables: { target },
});
if (!((_b = schemaResult.latestValidVersion) === null || _b === void 0 ? void 0 : _b.sdl)) {
throw new errors_1.SchemaNotFoundError();
}
const schema = (0, graphql_1.buildSchema)(schemaResult.latestValidVersion.sdl, {
assumeValidSDL: true,
assumeValid: true,
});
const typeInfo = new graphql_1.TypeInfo(schema);
const processedDocuments = [];
const allErrors = [];
for (const [index, operation] of operations.entries()) {
let document = null;
try {
document = (0, graphql_1.parse)(operation.content);
}
catch (error) {
allErrors.push({
source: new graphql_1.Source(operation.content, operation.operationHash),
errors: [error instanceof graphql_1.GraphQLError ? error : new graphql_1.GraphQLError(String(error))],
});
}
if (!document) {
continue;
}
const errors = (0, graphql_1.validate)(schema, document, [...graphql_1.specifiedRules, SingleOperationDefinitionRule]);
if (errors.length > 0) {
allErrors.push({
source: new graphql_1.Source(operation.content, operation.operationHash),
errors,
});
continue;
}
const operationName = getOperationName(document);
const schemaCoordinates = (0, core_1.collectSchemaCoordinates)({
documentNode: document,
processVariables: false,
variables: null,
schema,
typeInfo,
});
const processed = (0, core_1.preprocessOperation)({ document, operationName, schemaCoordinates });
if (!processed) {
allErrors.push({
source: new graphql_1.Source(operation.content, operation.operationHash),
errors: [
new graphql_1.GraphQLError(`Document at index ${index} has no executable operation definition.`),
],
});
continue;
}
processedDocuments.push({
hash: operation.operationHash,
body: operation.content,
operationName,
schemaCoordinates: Array.from(schemaCoordinates),
insightsHash: processed.hash,
});
}
if (allErrors.length) {
throw new errors_1.InvalidPersistedDocumentsError(allErrors);
}
const result = await this.registryApi(endpoint, accessToken).request({
operation: CreateAppDeploymentMutation,
variables: {
input: {
appName: flags['name'],
appVersion: version,
target,
},
},
});
if (result.createAppDeployment.error) {
const { message, details } = result.createAppDeployment.error;
const reasons = [
(details === null || details === void 0 ? void 0 : details.appName) ? `--name: ${details.appName}` : null,
(details === null || details === void 0 ? void 0 : details.appVersion) ? `--version: ${details.appVersion}` : null,
].filter(Boolean);
throw new errors_1.APIError(reasons.length ? `${message}\n${reasons.join('\n')}` : message);
}
if (!result.createAppDeployment.ok) {
throw new errors_1.APIError(`Create App failed without providing a reason.`);
}
if (result.createAppDeployment.ok.createdAppDeployment.status !== graphql_2.AppDeploymentStatus.Pending) {
this.log(`App deployment "${flags['name']}@${version}" is "${result.createAppDeployment.ok.createdAppDeployment.status}". Skip uploading documents...`);
return;
}
const totalDocuments = Object.keys(manifest).length;
this.log(`App deployment "${flags['name']}@${version}" is created pending document upload. Uploading documents...`);
let buffer = [];
let counter = 0;
const flush = async (force = false) => {
if (buffer.length >= 100 || (force && buffer.length > 0)) {
const result = await this.registryApi(endpoint, accessToken).request({
operation: AddDocumentsToAppDeploymentMutation,
timeout: 55000,
retry: { retries: 2 },
variables: {
input: {
target,
appName: flags['name'],
appVersion: version,
documents: buffer,
},
},
});
if (result.addPreprocessedDocumentsToAppDeployment.error) {
if (result.addPreprocessedDocumentsToAppDeployment.error.details) {
const affectedOperation = buffer[result.addPreprocessedDocumentsToAppDeployment.error.details.index];
const maxCharacters = 40;
if (affectedOperation) {
const truncatedBody = (affectedOperation.body.length > maxCharacters - 3
? affectedOperation.body.substring(0, maxCharacters) + '...'
: affectedOperation.body).replace(/\n/g, '\\n');
this.logWarning(`Failed uploading document: ${result.addPreprocessedDocumentsToAppDeployment.error.details.message}` +
`\nOperation hash: ${affectedOperation === null || affectedOperation === void 0 ? void 0 : affectedOperation.hash}` +
`\nOperation body: ${truncatedBody}`);
}
}
throw new errors_1.APIError(result.addPreprocessedDocumentsToAppDeployment.error.message);
}
buffer = [];
// don't bother showing 100% since there's another log line when it's done. And for deployments with just a few docs, showing this progress is unnecessary.
if (counter !== totalDocuments) {
this.log(`${counter} / ${totalDocuments} (${Math.round((100.0 * counter) / totalDocuments)}%) documents uploaded...`);
}
}
};
for (const document of processedDocuments) {
buffer.push(document);
counter++;
await flush();
}
await flush(true);
this.log(`\nApp deployment "${flags['name']}@${version}" (${counter} operations) created.`);
if (!flags.publish) {
this.log(`Activate it with the "hive app:publish" command.`);
return;
}
this.log('Publishing app deployment...');
const publishResult = await this.registryApi(endpoint, accessToken).request({
operation: publish_1.ActivateAppDeploymentMutation,
variables: {
input: {
target,
appName: flags['name'],
appVersion: version,
},
},
});
if (publishResult.activateAppDeployment.error) {
throw new errors_1.APIError(publishResult.activateAppDeployment.error.message);
}
if (publishResult.activateAppDeployment.ok) {
const deploymentName = `${publishResult.activateAppDeployment.ok.activatedAppDeployment.name}@${publishResult.activateAppDeployment.ok.activatedAppDeployment.version}`;
if (publishResult.activateAppDeployment.ok.isSkipped) {
this.warn(`\nApp deployment "${deploymentName}" is already published. Skipping...`);
}
else {
this.log('\nApp deployment published successfully.');
}
}
}
}
AppCreate.description = 'create an app deployment';
AppCreate.flags = {
'registry.endpoint': core_2.Flags.string({
description: 'registry endpoint',
}),
'registry.accessToken': core_2.Flags.string({
description: 'registry access token',
}),
name: core_2.Flags.string({
description: 'app name',
required: true,
}),
version: core_2.Flags.string({
description: 'app version',
}),
target: core_2.Flags.string({
description: 'The target in which the app deployment will be created.' +
' This can either be a slug following the format "$organizationSlug/$projectSlug/$targetSlug" (e.g "the-guild/graphql-hive/staging")' +
' or an UUID (e.g. "a0f4c605-6541-4350-8cfe-b31f21a4bf80").',
}),
publish: core_2.Flags.boolean({
description: 'Publish the app deployment after creation.',
default: false,
}),
};
AppCreate.args = {
operations: core_2.Args.string({
name: 'operations',
required: true,
description: 'Path to the persisted operations manifest (GraphQL Code Generator, Relay or Apollo persisted query manifest JSON file), a directory containing .graphql files, or a glob pattern matching .graphql files.',
hidden: false,
}),
};
exports.default = AppCreate;
const CreateAppDeploymentMutation = (0, gql_1.graphql)(/* GraphQL */ `
mutation CreateAppDeployment($input: CreateAppDeploymentInput!) {
createAppDeployment(input: $input) {
ok {
createdAppDeployment {
id
name
version
status
}
}
error {
message
details {
appName
appVersion
}
}
}
}
`);
const AddDocumentsToAppDeploymentMutation = (0, gql_1.graphql)(/* GraphQL */ `
mutation AddDocumentsToAppDeployment($input: AddPreprocessedDocumentsToAppDeploymentInput!) {
addPreprocessedDocumentsToAppDeployment(input: $input) {
ok {
appDeployment {
id
name
version
status
}
}
error {
message
details {
index
message
__typename
}
}
}
}
`);
function SingleOperationDefinitionRule(context) {
return {
Document(node) {
const operations = node.definitions.filter(definition => definition.kind === graphql_1.Kind.OPERATION_DEFINITION);
if (operations.length > 1) {
context.reportError(new graphql_1.GraphQLError('Multiple operation definitions found. Only one executable operation definition is allowed per document.', { nodes: operations }));
}
},
};
}
function getOperationName(document) {
var _a, _b;
for (const definition of document.definitions) {
if (definition.kind === graphql_1.Kind.OPERATION_DEFINITION) {
return (_b = (_a = definition.name) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
}
}
return null;
}
//# sourceMappingURL=create.js.map