Skip to content
17 changes: 15 additions & 2 deletions src/dataconnect/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import { select } from "../prompt";
import * as utils from "../utils";
import { prettify, prettifyTable } from "./graphqlError";
import { DeploymentMetadata, GraphqlError } from "./types";
import { DeploymentMetadata, GraphqlError, DeployStats } from "./types";
import { getProjectDefaultAccount } from "../auth";

export async function build(

Check warning on line 10 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
options: Options,
configDir: string,
deployStats: DeployStats,
dryRun?: boolean,
): Promise<DeploymentMetadata> {
const account = getProjectDefaultAccount(options.projectRoot);
Expand All @@ -19,15 +20,22 @@
}
const buildResult = await DataConnectEmulator.build(args);
if (buildResult?.errors?.length) {
await handleBuildErrors(buildResult.errors, options.nonInteractive, options.force, dryRun);
await handleBuildErrors(
buildResult.errors,
options.nonInteractive,
options.force,
deployStats,
dryRun,
);
}
return buildResult?.metadata ?? {};
}

export async function handleBuildErrors(

Check warning on line 34 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment

Check warning on line 34 in src/dataconnect/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
errors: GraphqlError[],
nonInteractive: boolean,
force: boolean,
deployStats: DeployStats,
dryRun?: boolean,
) {
if (errors.filter((w) => !w.extensions?.warningLevel).length) {
Expand Down Expand Up @@ -62,6 +70,7 @@
prettifyTable(requiredAcks),
);
if (nonInteractive && !force) {
deployStats.abort_build_warning = "true";
throw new FirebaseError(
"Explicit acknowledgement required for breaking schema or connector changes and new insecure operations. Rerun this command with --force to deploy these changes.",
);
Expand All @@ -72,9 +81,11 @@
default: "abort",
});
if (result === "abort") {
deployStats.abort_build_warning = "true";
throw new FirebaseError(`Deployment aborted.`);
}
}
deployStats.ack_build_warning = "true";
}
if (interactiveAcks.length) {
// This category contains WARNING and EXISTING_INSECURE issues.
Expand All @@ -90,8 +101,10 @@
default: "proceed",
});
if (result === "abort") {
deployStats.abort_build_warning = "true";
throw new FirebaseError(`Deployment aborted.`);
}
}
deployStats.ack_build_warning = "true";
}
}
34 changes: 31 additions & 3 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

Check failure on line 1 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Delete `⏎`

Check failure on line 1 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `⏎`

Check failure on line 1 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Delete `⏎`

Check failure on line 1 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Delete `⏎`

Check failure on line 1 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `⏎`
import * as clc from "colorette";
import { format } from "sql-formatter";

Expand All @@ -20,7 +21,7 @@
import { DEFAULT_SCHEMA, firebaseowner } from "../gcp/cloudsql/permissions";
import { select, confirm } from "../prompt";
import { logger } from "../logger";
import { Schema } from "./types";
import { Schema, DeployStats } from "./types";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
Expand Down Expand Up @@ -56,16 +57,16 @@
/* silent=*/ true,
);
default:
throw new FirebaseError(`Unexpected schema setup status: ${schemaInfo.setupStatus}`);

Check warning on line 60 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "never" of template literal expression
}
} catch (err: any) {

Check warning on line 62 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
throw new FirebaseError(
`Cannot setup Postgres SQL permissions of Cloud SQL database ${instanceId}:${databaseId}\n${err}`,

Check warning on line 64 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "any" of template literal expression
);
}
}

export async function diffSchema(

Check warning on line 69 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
options: Options,
schema: Schema,
schemaValidation?: SchemaValidation,
Expand All @@ -87,8 +88,8 @@
try {
await upsertSchema(schema, /** validateOnly=*/ true);
displayNoSchemaDiff(instanceId, databaseId, validationMode);
} catch (err: any) {

Check warning on line 91 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err?.status !== 400) {

Check warning on line 92 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
throw err;
}
incompatible = errors.getIncompatibleSchemaError(err);
Expand Down Expand Up @@ -123,7 +124,7 @@
displayStartSchemaDiff(validationMode);
await upsertSchema(schema, /** validateOnly=*/ true);
displayNoSchemaDiff(instanceId, databaseId, validationMode);
} catch (err: any) {

Check warning on line 127 in src/dataconnect/schemaMigration.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err?.status !== 400) {
throw err;
}
Expand All @@ -150,8 +151,9 @@
/** true for `dataconnect:sql:migrate`, false for `deploy` */
validateOnly: boolean;
schemaValidation?: SchemaValidation;
analytics?: DeployStats;
}): Promise<Diff[]> {
const { options, schema, validateOnly, schemaValidation } = args;
const { options, schema, validateOnly, schemaValidation, analytics } = args;

// If the schema validation mode is unset, we prompt COMPATIBLE SQL diffs and then STRICT diffs.
let validationMode: SchemaValidation = schemaValidation ?? "COMPATIBLE";
Expand All @@ -170,6 +172,9 @@
// Check if Cloud SQL instance is still being created.
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
if (existingInstance.state === "PENDING_CREATE") {
if (analytics) {
analytics.skip_pending_create = "true";
}
const postgresql = schema.datasources.find((d) => d.postgresql)?.postgresql;
if (!postgresql) {
throw new FirebaseError(
Expand Down Expand Up @@ -219,13 +224,15 @@
incompatible,
validateOnly,
validationMode,
analytics,
);

const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(
options,
serviceName,
invalidConnectors,
validateOnly,
analytics,
);

if (incompatible) {
Expand All @@ -235,10 +242,14 @@
instanceId,
incompatibleSchemaError: incompatible,
choice: migrationMode,
analytics,
});
}

if (shouldDeleteInvalidConnectors) {
if (analytics) {
analytics.delete_invalid_connector = "true";
}
await deleteInvalidConnectors(invalidConnectors);
}
if (!validateOnly) {
Expand Down Expand Up @@ -272,6 +283,7 @@
incompatible,
validateOnly,
"STRICT_AFTER_COMPATIBLE",
analytics,
);

if (incompatible) {
Expand All @@ -281,6 +293,7 @@
instanceId,
incompatibleSchemaError: incompatible,
choice: migrationMode,
analytics,
});
diffs = diffs.concat(maybeDiffs);
}
Expand Down Expand Up @@ -381,8 +394,9 @@
instanceId: string;
databaseId: string;
choice: "all" | "safe" | "none";
analytics?: DeployStats;
}): Promise<Diff[]> {
const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args;
const { incompatibleSchemaError, options, instanceId, databaseId, choice, analytics } = args;
const commandsToExecute = incompatibleSchemaError.diffs.filter((d) => {
switch (choice) {
case "all":
Expand All @@ -393,6 +407,9 @@
return false;
}
});
if (analytics) {
analytics.completed_schema_migration = "true";
}
if (commandsToExecute.length) {
const commandsToExecuteBySuperUser = commandsToExecute.filter(requireSuperUser);
const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !requireSuperUser(sql));
Expand Down Expand Up @@ -466,8 +483,12 @@
err: IncompatibleSqlSchemaError | undefined,
validateOnly: boolean,
validationMode: SchemaValidation | "STRICT_AFTER_COMPATIBLE",
analytics?: DeployStats,
): Promise<"none" | "safe" | "all"> {
if (!err) {
if (analytics) {
analytics.completed_schema_migration = "true";
}
return "none";
}
const defaultChoice = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
Expand Down Expand Up @@ -498,6 +519,9 @@
default: defaultChoice,
});
if (ans === "abort") {
if (analytics) {
analytics.abort_schema_migration = "true";
}
throw new FirebaseError("Command aborted.");
}
return ans;
Expand Down Expand Up @@ -526,6 +550,7 @@
serviceName: string,
invalidConnectors: string[],
validateOnly: boolean,
analytics?: DeployStats,
): Promise<boolean> {
if (!invalidConnectors.length) {
return false;
Expand All @@ -550,6 +575,9 @@
return true;
}
const cmd = suggestedCommand(serviceName, invalidConnectors);
if (analytics) {
analytics.abort_invalid_connector = "true";
}
throw new FirebaseError(
`Command aborted. Try deploying those connectors first with ${clc.bold(cmd)}`,
);
Expand Down
34 changes: 34 additions & 0 deletions src/dataconnect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,37 @@ interface ImpersonationUnauthenticated {
export type Impersonation = ImpersonationAuthenticated | ImpersonationUnauthenticated;

/** End Dataplane Client Types */

export interface DeployStats {
// prepare.ts
abort_missing_billing?: string;
abort_build_error?: string;
abort_build_warning?: string;
ack_build_warning?: string;

// deploy.ts
num_service_created: number;
num_service_deleted: number;

// release.ts
num_schema_migrated: number;
num_connector_updated_before_schema: number;
num_connector_updated_after_schema: number;

// migrateSchema.ts
skip_pending_create?: string;
abort_schema_migration?: string;
completed_schema_migration?: string;
abort_invalid_connector?: string;
delete_invalid_connector?: string;
}

export function initDeployStats(): DeployStats {
return {
num_service_created: 0,
num_service_deleted: 0,
num_schema_migrated: 0,
num_connector_updated_before_schema: 0,
num_connector_updated_after_schema: 0,
};
}
13 changes: 8 additions & 5 deletions src/deploy/dataconnect/deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as ensureApiEnabled from "../../ensureApiEnabled";
import * as prompt from "../../prompt";
import * as poller from "../../operation-poller";
import { dataconnectOrigin } from "../../api";
import { initDeployStats } from "../../dataconnect/types";

describe("dataconnect deploy", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -47,7 +48,7 @@ describe("dataconnect deploy", () => {
dataConnectYaml: { serviceId: "s1" },
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -66,7 +67,7 @@ describe("dataconnect deploy", () => {

confirmStub.resolves(true);
const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -83,7 +84,7 @@ describe("dataconnect deploy", () => {

confirmStub.resolves(false);
const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand Down Expand Up @@ -116,7 +117,7 @@ describe("dataconnect deploy", () => {
dataConnectYaml: { serviceId: "s1" },
},
];
const context = { dataconnect: { serviceInfos } };
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
const options = {} as any;

await deploy.default(context as any, options);
Expand All @@ -131,7 +132,9 @@ describe("dataconnect deploy", () => {
.reply(200, { services: existingServices });

const serviceInfos: any[] = [];
const context = { dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }] } };
const context = {
dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }], deployStats: initDeployStats() },
};
const options = {} as any;

await deploy.default(context as any, options);
Expand Down
5 changes: 4 additions & 1 deletion src/deploy/dataconnect/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Options } from "../../options";
import * as client from "../../dataconnect/client";
import * as utils from "../../utils";
import { Service, ServiceInfo, requiresVector } from "../../dataconnect/types";
import { Service, ServiceInfo, requiresVector, DeployStats } from "../../dataconnect/types";
import { needProjectId } from "../../projectUtils";
import { setupCloudSql } from "../../dataconnect/provisionCloudSql";
import { parseServiceName } from "../../dataconnect/names";
Expand All @@ -21,6 +21,7 @@ export default async function (
dataconnect: {
serviceInfos: ServiceInfo[];
filters?: ResourceFilter[];
deployStats: DeployStats;
};
},
options: Options,
Expand All @@ -43,10 +44,12 @@ export default async function (
.filter((si) => {
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
});
context.dataconnect.deployStats.num_service_created = servicesToCreate.length;

const servicesToDelete = filters
? []
: services.filter((s) => !serviceInfos.some((si) => matches(si, s)));
context.dataconnect.deployStats.num_service_deleted = servicesToDelete.length;
await Promise.all(
servicesToCreate.map(async (s) => {
const { projectId, locationId, serviceId } = splitName(s.serviceName);
Expand Down
18 changes: 16 additions & 2 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { setupCloudSql } from "../../dataconnect/provisionCloudSql";
import { checkBillingEnabled } from "../../gcp/cloudbilling";
import { parseServiceName } from "../../dataconnect/names";
import { FirebaseError } from "../../error";
import { requiresVector } from "../../dataconnect/types";
import { requiresVector, initDeployStats } from "../../dataconnect/types";
import { diffSchema } from "../../dataconnect/schemaMigration";
import { upgradeInstructions } from "../../dataconnect/freeTrial";

Expand All @@ -24,16 +24,30 @@ import { upgradeInstructions } from "../../dataconnect/freeTrial";
* @param options The CLI options object.
*/
export default async function (context: any, options: DeployOptions): Promise<void> {
context.dataconnect = {
deployStats: initDeployStats(),
};
const projectId = needProjectId(options);
if (!(await checkBillingEnabled(projectId))) {
context.dataconnect.deployStats.abort_missing_billing = "true";
throw new FirebaseError(upgradeInstructions(projectId));
}
await ensureApis(projectId);
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
const filters = getResourceFilters(options);
const serviceInfos = await loadAll(projectId, options.config);
for (const si of serviceInfos) {
si.deploymentMetadata = await build(options, si.sourceDirectory, options.dryRun);
try {
si.deploymentMetadata = await build(
options,
si.sourceDirectory,
context.dataconnect.deployStats,
options.dryRun,
);
} catch (e: any) {
context.dataconnect.deployStats.abort_build_error = "true";
throw e;
}
}
const unmatchedFilters = filters?.filter((f) => {
// filter out all filters that match no service
Expand Down
Loading
Loading