firebase-tools
Version:
Command-Line Interface for Firebase
230 lines (229 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.command = void 0;
const clc = require("colorette");
const command_1 = require("../command");
const projectUtils_1 = require("../projectUtils");
const load_1 = require("../dataconnect/load");
const requireAuth_1 = require("../requireAuth");
const constants_1 = require("../emulator/constants");
const apiv2_1 = require("../apiv2");
const dataplaneClient_1 = require("../dataconnect/dataplaneClient");
const dataplaneClient_2 = require("../dataconnect/dataplaneClient");
const names_1 = require("../dataconnect/names");
const error_1 = require("../error");
const node_fs_1 = require("node:fs");
const types_1 = require("../dataconnect/types");
const hub_1 = require("../emulator/hub");
const promises_1 = require("node:fs/promises");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const consumers_1 = require("node:stream/consumers");
const logger_1 = require("../logger");
const responseToError_1 = require("../responseToError");
let stdinUsedFor = undefined;
exports.command = new command_1.Command("dataconnect:execute [file] [operationName]")
.description("execute a Data Connect query or mutation. If FIREBASE_DATACONNECT_EMULATOR_HOST is set (such as during 'firebase emulator:exec', executes against the emulator instead.")
.option("--service <serviceId>", "The service ID to execute against (optional if there's only one service)")
.option("--location <locationId>", "The location ID to execute against (optional if there's only one service). Ignored by the emulator.")
.option("--vars, --variables <vars>", "Supply variables to the operation execution, which must be a JSON object whose keys are variable names. If vars begin with the character @, the rest is interpreted as a file name to read from, or - to read from stdin.")
.option("--no-debug-details", "Disables debug information in the response. Executions returns helpful errors or GQL extensions by default, which may expose too much for unprivilleged user or programs. If that's the case, this flag turns those output off.")
.action(async (file = "", operationName, options) => {
const emulatorHost = process.env[constants_1.Constants.FIREBASE_DATACONNECT_EMULATOR_HOST];
let projectId;
if (emulatorHost) {
projectId = (0, projectUtils_1.getProjectId)(options) || hub_1.EmulatorHub.MISSING_PROJECT_PLACEHOLDER;
}
else {
projectId = (0, projectUtils_1.needProjectId)(options);
}
let serviceName = undefined;
const serviceId = options.service;
const locationId = options.location;
if (!file && !operationName) {
if (process.stdin.isTTY) {
throw new error_1.FirebaseError("At least one of the [file] [operationName] arguments is required.");
}
file = "-";
}
let query;
if (file === "-") {
stdinUsedFor = "operation source code";
if (process.stdin.isTTY) {
process.stderr.write(`${clc.cyan("Reading GraphQL operation from stdin. EOF (CTRL+D) to finish and execute.")}${node_os_1.EOL}`);
}
query = await (0, consumers_1.text)(process.stdin);
}
else {
const stat = (0, node_fs_1.statSync)(file, { throwIfNoEntry: false });
if (stat === null || stat === void 0 ? void 0 : stat.isFile()) {
const opDisplay = operationName ? clc.bold(operationName) : "operation";
process.stderr.write(`${clc.cyan(`Executing ${opDisplay} in ${clc.bold(file)}`)}${node_os_1.EOL}`);
query = await (0, promises_1.readFile)(file, "utf-8");
}
else if (stat === null || stat === void 0 ? void 0 : stat.isDirectory()) {
query = await readQueryFromDir(file);
}
else {
if (operationName === undefined && (0, names_1.isGraphqlName)(file)) {
operationName = file;
file = "";
}
if (file) {
throw new error_1.FirebaseError(`${file}: no such file or directory`);
}
file = await pickConnectorDir();
query = await readQueryFromDir(file);
}
}
let apiClient;
if (emulatorHost) {
const url = new URL("http://placeholder");
url.host = emulatorHost;
apiClient = new apiv2_1.Client({
urlPrefix: url.toString(),
apiVersion: dataplaneClient_1.DATACONNECT_API_VERSION,
});
}
else {
await (0, requireAuth_1.requireAuth)(options);
apiClient = (0, dataplaneClient_2.dataconnectDataplaneClient)();
}
if (!serviceName) {
if (serviceId && (locationId || emulatorHost)) {
serviceName = `projects/${projectId}/locations/${locationId || "unused"}/services/${serviceId}`;
}
else {
serviceName = (await getServiceInfo()).serviceName;
}
}
if (!options.variables && !process.stdin.isTTY && !stdinUsedFor) {
options.variables = "@-";
}
const unparsedVars = await literalOrFile(options.variables, "--variables");
const response = await (0, dataplaneClient_1.executeGraphQL)(apiClient, serviceName, {
query,
operationName,
variables: parseJsonObject(unparsedVars, "--variables"),
});
let err = (0, responseToError_1.responseToError)(response, response.body);
if ((0, types_1.isGraphQLResponseError)(response.body)) {
const { status, message } = response.body.error;
if (!err) {
err = new error_1.FirebaseError(message, {
context: {
body: response.body,
response: response,
},
status: response.status,
});
}
if (status === "INVALID_ARGUMENT" && message.includes("operationName is required")) {
throw new error_1.FirebaseError(err.message + `\nHint: Append <operationName> as an argument to disambiguate.`, Object.assign(Object.assign({}, err), { original: err }));
}
}
if (err) {
throw err;
}
if (!(0, types_1.isGraphQLResponse)(response.body)) {
throw new error_1.FirebaseError("Got invalid response body with neither .data or .errors", {
context: {
body: response.body,
response: response,
},
status: response.status,
});
}
logger_1.logger.info(JSON.stringify(response.body, null, 2));
if (!response.body.data) {
throw new error_1.FirebaseError("GraphQL request error(s). See response body (above) for details.", {
context: {
body: response.body,
response: response,
},
status: response.status,
});
}
if (response.body.errors && response.body.errors.length > 0) {
throw new error_1.FirebaseError("Execution completed with error(s). See response body (above) for details.", {
context: {
body: response.body,
response: response,
},
status: response.status,
});
}
return response.body;
async function readQueryFromDir(dir) {
const opDisplay = operationName ? clc.bold(operationName) : "operation";
process.stderr.write(`${clc.cyan(`Executing ${opDisplay} in ${clc.bold(dir)}`)}${node_os_1.EOL}`);
const files = await (0, load_1.readGQLFiles)(dir);
const query = (0, load_1.squashGraphQL)({ files });
if (!query) {
throw new error_1.FirebaseError(`${dir} contains no GQL files or only empty ones`);
}
return query;
}
async function getServiceInfo() {
return (0, load_1.pickOneService)(projectId, options.config, serviceId || undefined, locationId || undefined).catch((e) => {
if (!(e instanceof error_1.FirebaseError)) {
return Promise.reject(e);
}
if (!serviceId) {
e = new error_1.FirebaseError(e.message +
`\nHint: Try specifying the ${clc.yellow("--service <serviceId>")} option.`, Object.assign(Object.assign({}, e), { original: e }));
}
return Promise.reject(e);
});
}
async function pickConnectorDir() {
const serviceInfo = await getServiceInfo();
serviceName = serviceInfo.serviceName;
switch (serviceInfo.connectorInfo.length) {
case 1: {
const connector = serviceInfo.connectorInfo[0];
return (0, node_path_1.relative)(process.cwd(), connector.directory);
}
case 0:
throw new error_1.FirebaseError(`No connector found.\n` +
"Hint: To execute an operation in a GraphQL file, run:\n" +
` firebase dataconnect:execute ${clc.yellow("./path/to/file.gql OPERATION_NAME")}`);
default: {
const example = (0, node_path_1.relative)(process.cwd(), serviceInfo.connectorInfo[0].directory);
throw new error_1.FirebaseError(`A file or directory must be explicitly specified when there are multiple connectors.\n` +
"Hint: To execute an operation within a connector, try e.g.:\n" +
` firebase dataconnect:execute ${clc.yellow(`${example} OPERATION_NAME`)}`);
}
}
}
});
function parseJsonObject(json, subject) {
let obj;
try {
obj = JSON.parse(json || "{}");
}
catch (e) {
throw new error_1.FirebaseError(`expected ${subject} to be valid JSON string, got: ${json}`);
}
if (typeof obj !== "object" || obj == null)
throw new error_1.FirebaseError(`Provided ${subject} is not an object`);
return obj;
}
async function literalOrFile(arg, subject) {
let str = arg;
if (!str) {
return "";
}
if (str.startsWith("@")) {
if (str === "@-") {
if (stdinUsedFor) {
throw new error_1.FirebaseError(`standard input can only be used for one of ${stdinUsedFor} and ${subject}.`);
}
str = await (0, consumers_1.text)(process.stdin);
}
else {
str = await (0, promises_1.readFile)(str.substring(1), "utf-8");
}
}
return str;
}