Skip to content
33 changes: 19 additions & 14 deletions src/dataconnect/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,27 @@ const PRECONDITION_ERROR_TYPESTRING = "type.googleapis.com/google.rpc.Preconditi
const INCOMPATIBLE_CONNECTOR_TYPE = "INCOMPATIBLE_CONNECTOR";

export function getIncompatibleSchemaError(err: any): IncompatibleSqlSchemaError | undefined {
const original = err.context?.body?.error || err.orignal;
if (!original) {
// If we can't get the original, rethrow so we don't cover up the original error.
throw err;
const incompatibles = errorDetails(err, INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING);
if (incompatibles.length === 0) {
return undefined;
}
const details: any[] = original.details;
const incompatibles = details.filter((d) =>
d["@type"]?.includes(INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING),
);
// Should never get multiple incompatible schema errors
return incompatibles[0];
const incompatible = incompatibles[0];
// Extract the violation type from the precondition error detail.
const preconditionErrs = errorDetails(err, PRECONDITION_ERROR_TYPESTRING);
const violationTypes = (incompatible.violationType = preconditionErrs
.flatMap((preCondErr) => preCondErr.violations)
.flatMap((viol) => viol.type)
.filter((type) => type === "INACCESSIBLE_SCHEMA" || type === "INCOMPATIBLE_SCHEMA"));
incompatible.violationType = violationTypes[0];
return incompatible;
}

// Note - the backend just includes file name, not the name of the connector resource in the GQLerror extensions.
// so we don't use this yet. Ideally, we'd just include connector name in the extensions.
export function getInvalidConnectors(err: any): string[] {
const preconditionErrs = errorDetails(err, PRECONDITION_ERROR_TYPESTRING);
const invalidConns: string[] = [];
const original = err.context?.body?.error || err?.orignal;
const details: any[] = original?.details;
const preconditionErrs = details?.filter((d) =>
d["@type"]?.includes(PRECONDITION_ERROR_TYPESTRING),
);
for (const preconditionErr of preconditionErrs) {
const incompatibleConnViolation = preconditionErr?.violations?.filter(
(v: { type: string }) => v.type === INCOMPATIBLE_CONNECTOR_TYPE,
Expand All @@ -36,3 +35,9 @@ export function getInvalidConnectors(err: any): string[] {
}
return invalidConns;
}

function errorDetails(err: any, ofType: string): any[] {
const original = err.context?.body?.error || err?.original;
const details: any[] = original?.details;
return details?.filter((d) => d["@type"]?.includes(ofType)) || [];
}
32 changes: 26 additions & 6 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function migrateSchema(args: {
if (!shouldDeleteInvalidConnectors && invalidConnectors.length) {
const cmd = suggestedCommand(serviceName, invalidConnectors);
throw new FirebaseError(
`Command aborted. Try deploying compatible connectors first with ${clc.bold(cmd)}`,
`Command aborted. Try deploying those connectors first with ${clc.bold(cmd)}`,
);
}
const migrationMode = incompatible
Expand Down Expand Up @@ -356,11 +356,31 @@ async function ensureServiceIsConnectedToCloudSql(
}

function displaySchemaChanges(error: IncompatibleSqlSchemaError) {
const message =
"Your new schema is incompatible with the schema of your CloudSQL database. " +
"The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
error.diffs.map(toString).join("\n");
logLabeledWarning("dataconnect", message);
switch (error.violationType) {
case "INCOMPATIBLE_SCHEMA":
{
const message =
"Your new schema is incompatible with the schema of your CloudSQL database. " +
"The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
error.diffs.map(toString).join("\n");
logLabeledWarning("dataconnect", message);
}
break;
case "INACCESSIBLE_SCHEMA":
{
const message =
"Cannot access your CloudSQL database to validate schema. " +
"The following SQL statements can setup a new database schema.\n" +
error.diffs.map(toString).join("\n");
logLabeledWarning("dataconnect", message);
logLabeledWarning("dataconnect", "Some SQL resources may already exist.");
}
break;
default:
throw new FirebaseError(
`Unknown schema violation type: ${error.violationType}, IncompatibleSqlSchemaError: ${error}`,
);
}
}

function toString(diff: Diff) {
Expand Down
7 changes: 5 additions & 2 deletions src/dataconnect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ export interface File {
content: string;
}

// An error indicating that the SQL database schema is incomptible with a data connect schema.
// An error indicating that the SQL database schema is incompatible with a data connect schema.
export interface IncompatibleSqlSchemaError {
// A list of differences between the two schema with instrucitons how to resolve them.
// A list of differences between the two schema with instructions how to resolve them.
diffs: Diff[];
// Whether any of the changes included are destructive.
destructive: boolean;

// The failed precondition validation type.
violationType: "INCOMPATIBLE_SCHEMA" | "INACCESSIBLE_SCHEMA" | string;
}

export interface Diff {
Expand Down