UNPKG

convex

Version:

Client for the Convex Cloud

242 lines (229 loc) 6.3 kB
import { Option } from "@commander-js/extra-typings"; import chalk from "chalk"; import { Context, logError, logOutput, logWarning, oneoffContext, } from "../bundler/context.js"; import { Base64 } from "../values/index.js"; import { Value } from "../values/value.js"; import { deploymentSelectionFromOptions, fetchDeploymentCredentialsProvisionProd, } from "./lib/api.js"; import { runPaginatedQuery } from "./lib/run.js"; import { parsePositiveInteger } from "./lib/utils/utils.js"; import { Command } from "@commander-js/extra-typings"; import { actionDescription } from "./lib/command.js"; export const data = new Command("data") .summary("List tables and print data from your database") .description( "Inspect your Convex deployment's database.\n\n" + " List tables: `npx convex data`\n" + " List documents in a table: `npx convex data tableName`\n\n" + "By default, this inspects your dev deployment.", ) .argument("[table]", "If specified, list documents in this table.") .addOption( new Option( "--limit <n>", "List only the `n` the most recently created documents.", ) .default(100) .argParser(parsePositiveInteger), ) .addOption( new Option( "--order <choice>", "Order the documents by their `_creationTime`.", ) .choices(["asc", "desc"]) .default("desc"), ) .addDeploymentSelectionOptions(actionDescription("Inspect the database in")) .showHelpAfterError() .action(async (tableName, options) => { const ctx = oneoffContext; const deploymentSelection = deploymentSelectionFromOptions(options); const { adminKey, url: deploymentUrl, deploymentName, } = await fetchDeploymentCredentialsProvisionProd(ctx, deploymentSelection); if (tableName !== undefined) { await listDocuments(ctx, deploymentUrl, adminKey, tableName, { ...options, order: options.order as "asc" | "desc", }); } else { await listTables(ctx, deploymentUrl, adminKey, deploymentName); } }); async function listTables( ctx: Context, deploymentUrl: string, adminKey: string, deploymentName: string | undefined, ) { const tables = (await runPaginatedQuery( ctx, deploymentUrl, adminKey, "_system/cli/tables", {}, )) as { name: string }[]; if (tables.length === 0) { logError( ctx, `There are no tables in the ${ deploymentName ? `${chalk.bold(deploymentName)} deployment's ` : "" }database.`, ); return; } const tableNames = tables.map((table) => table.name); tableNames.sort(); logOutput(ctx, tableNames.join("\n")); } async function listDocuments( ctx: Context, deploymentUrl: string, adminKey: string, tableName: string, options: { limit: number; order: "asc" | "desc"; }, ) { const data = (await runPaginatedQuery( ctx, deploymentUrl, adminKey, "_system/cli/tableData", { table: tableName, order: options.order ?? "desc", }, options.limit + 1, )) as Record<string, Value>[]; if (data.length === 0) { logError(ctx, "There are no documents in this table."); return; } logDocumentsTable( ctx, data.slice(0, options.limit).map((document) => { const printed: Record<string, string> = {}; for (const key in document) { printed[key] = stringify(document[key]); } return printed; }), ); if (data.length > options.limit) { logWarning( ctx, chalk.yellow( `Showing the ${options.limit} ${ options.order === "desc" ? "most recently" : "oldest" } created document${ options.limit > 1 ? "s" : "" }. Use the --limit option to see more.`, ), ); } } function logDocumentsTable(ctx: Context, rows: Record<string, string>[]) { const columnsToWidths: Record<string, number> = {}; for (const row of rows) { for (const column in row) { const value = row[column]; columnsToWidths[column] = Math.max( value.length, columnsToWidths[column] ?? 0, ); } } const unsortedFields = Object.keys(columnsToWidths); unsortedFields.sort(); const fields = Array.from( new Set(["_id", "_creationTime", ...unsortedFields]), ); const columnWidths = fields.map((field) => columnsToWidths[field]); const lineLimit = process.stdout.isTTY ? process.stdout.columns : undefined; let didTruncate = false; function limitLine(line: string, limit: number | undefined) { if (limit === undefined) { return line; } const limitWithBufferForUnicode = limit - 10; if (line.length > limitWithBufferForUnicode) { didTruncate = true; } return line.slice(0, limitWithBufferForUnicode); } logOutput( ctx, limitLine( fields.map((field, i) => field.padEnd(columnWidths[i])).join(" | "), lineLimit, ), ); logOutput( ctx, limitLine( columnWidths.map((width) => "-".repeat(width)).join("-|-"), lineLimit, ), ); for (const row of rows) { logOutput( ctx, limitLine( fields .map((field, i) => (row[field] ?? "").padEnd(columnWidths[i])) .join(" | "), lineLimit, ), ); } if (didTruncate) { logWarning( ctx, chalk.yellow( "Lines were truncated to fit the terminal width. Pipe the command to see " + "the full output, such as:\n `npx convex data tableName | less -S`", ), ); } } function stringify(value: Value): string { if (value === null) { return "null"; } if (typeof value === "bigint") { return `${value.toString()}n`; } if (typeof value === "number") { return value.toString(); } if (typeof value === "boolean") { return value.toString(); } if (typeof value === "string") { return JSON.stringify(value); } if (value instanceof ArrayBuffer) { const base64Encoded = Base64.fromByteArray(new Uint8Array(value)); return `Bytes("${base64Encoded}")`; } if (value instanceof Array) { return `[${value.map(stringify).join(", ")}]`; } const pairs = Object.entries(value) .map(([k, v]) => `"${k}": ${stringify(v!)}`) .join(", "); return `{ ${pairs} }`; }