UNPKG

convex

Version:

Client for the Convex Cloud

264 lines (255 loc) 8.13 kB
import { changeSpinner, Context, logError, logFailure, } from "../../bundler/context.js"; import { deploymentFetch, ErrorData, logAndHandleFetchError, ThrowingFetchError, } from "./utils/utils.js"; import { schemaStatus, SchemaStatus, StartPushRequest, startPushResponse, StartPushResponse, } from "./deployApi/startPush.js"; import { AppDefinitionConfig, ComponentDefinitionConfig, } from "./deployApi/definitionConfig.js"; import chalk from "chalk"; import { getTargetDeploymentName } from "./deployment.js"; import { deploymentDashboardUrlPage } from "../dashboard.js"; import { finishPushDiff, FinishPushDiff } from "./deployApi/finishPush.js"; import { Reporter, Span } from "./tracing.js"; /** Push configuration2 to the given remote origin. */ export async function startPush( ctx: Context, span: Span, request: StartPushRequest, options: { url: string; verbose?: boolean; }, ): Promise<StartPushResponse> { if (options.verbose) { const custom = (_k: string | number, s: any) => typeof s === "string" ? s.slice(0, 40) + (s.length > 40 ? "..." : "") : s; console.log(JSON.stringify(request, custom, 2)); } const onError = (err: any) => { if (err.toString() === "TypeError: fetch failed") { changeSpinner( ctx, `Fetch failed, is ${options.url} correct? Retrying...`, ); } }; const fetch = deploymentFetch(options.url, request.adminKey, onError); changeSpinner(ctx, "Analyzing and deploying source code..."); try { const response = await fetch("/api/deploy2/start_push", { body: JSON.stringify(request), method: "POST", headers: { traceparent: span.encodeW3CTraceparent(), }, }); return startPushResponse.parse(await response.json()); } catch (error: unknown) { const data: ErrorData | undefined = error instanceof ThrowingFetchError ? error.serverErrorData : undefined; if (data?.code === "AuthConfigMissingEnvironmentVariable") { const errorMessage = data.message || "(no error message given)"; // If `npx convex dev` is running using --url there might not be a configured deployment const configuredDeployment = getTargetDeploymentName(); const [, variableName] = errorMessage.match(/Environment variable (\S+)/i) ?? []; const variableQuery = variableName !== undefined ? `?var=${variableName}` : ""; const dashboardUrl = deploymentDashboardUrlPage( configuredDeployment, `/settings/environment-variables${variableQuery}`, ); const message = `Environment variable ${chalk.bold( variableName, )} is used in auth config file but ` + `its value was not set. Go to:\n\n ${chalk.bold( dashboardUrl, )}\n\n to set it up. `; await ctx.crash({ exitCode: 1, errorType: "invalid filesystem or env vars", errForSentry: error, printedMessage: message, }); } logFailure(ctx, "Error: Unable to start push to " + options.url); return await logAndHandleFetchError(ctx, error); } } // Long poll every 10s for progress on schema validation. const SCHEMA_TIMEOUT_MS = 10_000; export async function waitForSchema( ctx: Context, span: Span, startPush: StartPushResponse, options: { adminKey: string; url: string; dryRun: boolean; }, ) { const fetch = deploymentFetch(options.url, options.adminKey); changeSpinner( ctx, "Backfilling indexes and checking that documents match your schema...", ); while (true) { let currentStatus: SchemaStatus; try { const response = await fetch("/api/deploy2/wait_for_schema", { body: JSON.stringify({ adminKey: options.adminKey, schemaChange: startPush.schemaChange, timeoutMs: SCHEMA_TIMEOUT_MS, dryRun: options.dryRun, }), method: "POST", headers: { traceparent: span.encodeW3CTraceparent(), }, }); currentStatus = schemaStatus.parse(await response.json()); } catch (error: unknown) { logFailure(ctx, "Error: Unable to wait for schema from " + options.url); return await logAndHandleFetchError(ctx, error); } switch (currentStatus.type) { case "inProgress": { let schemaDone = true; let indexesComplete = 0; let indexesTotal = 0; for (const componentStatus of Object.values(currentStatus.components)) { if (!componentStatus.schemaValidationComplete) { schemaDone = false; } indexesComplete += componentStatus.indexesComplete; indexesTotal += componentStatus.indexesTotal; } const indexesDone = indexesComplete === indexesTotal; let msg: string; if (!indexesDone && !schemaDone) { msg = `Backfilling indexes (${indexesComplete}/${indexesTotal} ready) and checking that documents match your schema...`; } else if (!indexesDone) { msg = `Backfilling indexes (${indexesComplete}/${indexesTotal} ready)...`; } else { msg = "Checking that documents match your schema..."; } changeSpinner(ctx, msg); break; } case "failed": { // Schema validation failed. This could be either because the data // is bad or the schema is wrong. Classify this as a filesystem error // because adjusting `schema.ts` is the most normal next step. let msg = "Schema validation failed"; if (currentStatus.componentPath) { msg += ` in component "${currentStatus.componentPath}"`; } msg += "."; logFailure(ctx, msg); logError(ctx, chalk.red(`${currentStatus.error}`)); return await ctx.crash({ exitCode: 1, errorType: { "invalid filesystem or db data": currentStatus.tableName ? { tableName: currentStatus.tableName, componentPath: currentStatus.componentPath, } : null, }, printedMessage: null, // TODO - move logging into here }); } case "raceDetected": { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Schema was overwritten by another push.`, }); } case "complete": { changeSpinner(ctx, "Schema validation complete."); return; } } } } export async function finishPush( ctx: Context, span: Span, startPush: StartPushResponse, options: { adminKey: string; url: string; dryRun: boolean; }, ): Promise<FinishPushDiff> { changeSpinner(ctx, "Finalizing push..."); const fetch = deploymentFetch(options.url, options.adminKey); try { const response = await fetch("/api/deploy2/finish_push", { body: JSON.stringify({ adminKey: options.adminKey, startPush, dryRun: options.dryRun, }), method: "POST", headers: { traceparent: span.encodeW3CTraceparent(), }, }); return finishPushDiff.parse(await response.json()); } catch (error: unknown) { logFailure(ctx, "Error: Unable to finish push to " + options.url); return await logAndHandleFetchError(ctx, error); } } export type ComponentDefinitionConfigWithoutImpls = Omit< ComponentDefinitionConfig, "schema" | "functions" >; export type AppDefinitionConfigWithoutImpls = Omit< AppDefinitionConfig, "schema" | "functions" | "auth" >; export async function reportPushCompleted( ctx: Context, adminKey: string, url: string, reporter: Reporter, ) { const fetch = deploymentFetch(url, adminKey); try { const response = await fetch("/api/deploy2/report_push_completed", { body: JSON.stringify({ adminKey, spans: reporter.spans, }), method: "POST", }); await response.json(); } catch (error: unknown) { logFailure( ctx, "Error: Unable to report push completed to " + url + ": " + error, ); } }