UNPKG

@graphql-hive/cli

Version:

A CLI util to manage and control your GraphQL Hive

301 lines • 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const node_crypto_1 = require("node:crypto"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const graphql_1 = require("graphql"); const zod_1 = require("zod"); const graphql_file_loader_1 = require("@graphql-tools/graphql-file-loader"); const load_1 = require("@graphql-tools/load"); const core_1 = 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 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"); 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) { this.logDebug(e); throw new errors_1.MissingEndpointError(); } try { accessToken = this.ensure({ key: 'registry.accessToken', args: flags, env: 'HIVE_TOKEN', description: AppCreate.flags['registry.accessToken'].description, }); } catch (e) { this.logDebug(e); throw new errors_1.MissingRegistryTokenError(); } 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 file = args.operations; let manifest; const isFile = (() => { try { return (0, node_fs_1.statSync)(file).isFile(); } catch (_a) { return false; } })(); if (isFile) { const contents = this.readJSON(file); const operations = JSON.parse(contents); const validationResult = ManifestModel.safeParse(operations); if (validationResult.success === false) { throw new errors_1.PersistedOperationsMalformedError(file); } manifest = validationResult.data; } else { // file is a glob or directory - generate the manifest in-memory const globPattern = (() => { try { if ((0, node_fs_1.statSync)(file).isDirectory()) { return `${(0, node_path_1.resolve)(file)}/**/*.graphql`; } } catch (_a) { // not a directory, treat as a glob pattern as-is } return file; })(); let sources; try { sources = await (0, load_1.loadDocuments)(globPattern, { loaders: [new graphql_file_loader_1.GraphQLFileLoader()], }); } catch (err) { this.error(`Failed to load GraphQL files from "${(0, node_path_1.relative)(process.cwd(), file)}": ${String(err)}`); } if (sources.length === 0) { this.error(`No .graphql files found in "${(0, node_path_1.relative)(process.cwd(), file)}".`); } // sort by location to make the output deterministic sources.sort((a, b) => { var _a, _b; return ((_a = a.location) !== null && _a !== void 0 ? _a : '').localeCompare((_b = b.location) !== null && _b !== void 0 ? _b : ''); }); manifest = {}; for (const source of sources) { const sourceFile = (_b = source.location) !== null && _b !== void 0 ? _b : '<unknown>'; if (!source.document) { this.warn(`Skipping empty operation in file "${(0, node_path_1.relative)(process.cwd(), sourceFile)}".`); continue; } const operation = (0, graphql_1.print)(source.document).replace('\n', ' ').replace(/\s+/g, ' ').trim(); if (!operation) { this.warn(`Skipping empty operation in file "${(0, node_path_1.relative)(process.cwd(), sourceFile)}".`); continue; } const hash = (0, node_crypto_1.createHash)('sha256').update(operation).digest('hex'); if (hash in manifest) { this.warn(`Hash collision detected for file "${(0, node_path_1.relative)(process.cwd(), sourceFile)}". The operation is identical to another operation already in the manifest. Skipping.`); continue; } manifest[hash] = operation; } if (Object.keys(manifest).length === 0) { this.error(`No valid GraphQL operations found in "${(0, node_path_1.relative)(process.cwd(), file)}".`); } this.log(`Persisted documents manifest generated in-memory from discovered GraphQL operations under "${globPattern}".`); this.log(JSON.stringify(manifest, null, 2)); } const result = await this.registryApi(endpoint, accessToken).request({ operation: CreateAppDeploymentMutation, variables: { input: { appName: flags['name'], appVersion: version, target, }, }, }); if (result.createAppDeployment.error) { throw new errors_1.APIError(result.createAppDeployment.error.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, variables: { input: { target, appName: flags['name'], appVersion: version, documents: buffer, }, }, }); if (result.addDocumentsToAppDeployment.error) { if (result.addDocumentsToAppDeployment.error.details) { const affectedOperation = buffer[result.addDocumentsToAppDeployment.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.addDocumentsToAppDeployment.error.details.message}` + `\nOperation hash: ${affectedOperation === null || affectedOperation === void 0 ? void 0 : affectedOperation.hash}` + `\nOperation body: ${truncatedBody}`); } } throw new errors_1.APIError(result.addDocumentsToAppDeployment.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 [hash, body] of Object.entries(manifest)) { buffer.push({ hash, body }); 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_1.Flags.string({ description: 'registry endpoint', }), 'registry.accessToken': core_1.Flags.string({ description: 'registry access token', }), name: core_1.Flags.string({ description: 'app name', required: true, }), version: core_1.Flags.string({ description: 'app version', }), target: core_1.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_1.Flags.boolean({ description: 'Publish the app deployment after creation.', default: false, }), }; AppCreate.args = { operations: core_1.Args.string({ name: 'operations', required: true, description: 'Path to the persisted operations manifest (JSON file), a directory containing .graphql files, or a glob pattern matching .graphql files.', hidden: false, }), }; exports.default = AppCreate; const ManifestModel = zod_1.z.record(zod_1.z.string()); const CreateAppDeploymentMutation = (0, gql_1.graphql)(/* GraphQL */ ` mutation CreateAppDeployment($input: CreateAppDeploymentInput!) { createAppDeployment(input: $input) { ok { createdAppDeployment { id name version status } } error { message } } } `); const AddDocumentsToAppDeploymentMutation = (0, gql_1.graphql)(/* GraphQL */ ` mutation AddDocumentsToAppDeployment($input: AddDocumentsToAppDeploymentInput!) { addDocumentsToAppDeployment(input: $input) { ok { appDeployment { id name version status } } error { message details { index message __typename } } } } `); //# sourceMappingURL=create.js.map