convex
Version:
Client for the Convex Cloud
264 lines (255 loc) • 8.13 kB
text/typescript
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,
);
}
}